Commit ·
81c58cc
1
Parent(s): b7e415b
Phase 12: Fix 6 batches — grammar pipeline bypass, religious/structured protection, punct rearrangement, SV/gender agreement
Browse filesBatch 1: Grammar IV->OOV bypass for case/nasb/five_nouns/dual patterns (14 FN)
Batch 2: Religious text now skips ALL stages including spelling (9 FP)
Batch 3: Expanded HAMZA_WHITELIST with 30+ new entries
Batch 4: Reject punct rearrangement + mid-sentence insertion (4 FP)
Batch 5: Skip spelling for URLs/emails/Latin/hashtags (7+4 FP)
Batch 6: Fix present-tense SV agreement + noun-adj gender (15 FN)
Also: Added directional blocks for كانت→كأنت (H016)
- docs/Chapter_1_Introduction.md +290 -0
- docs/Chapter_2_Literature_Review.md +194 -0
- docs/Chapter_3_System_Design.md +655 -0
- docs/Chapter_4_Implementation.md +658 -0
- docs/Chapter_5_Testing_and_Evaluation.md +393 -0
- docs/Chapter_6_Results_and_Discussion.md +340 -0
- docs/Chapter_7_Conclusion_and_Future_Work.md +211 -0
- exhaustive_check.py +165 -125
- investigate.py +31 -0
- phase8_results.json +1702 -0
- src/app.py +94 -27
- src/nlp/grammar/grammar_rules.py +54 -7
- src/nlp/punctuation/punctuation_rules.py +39 -0
- src/nlp/spelling/araspell_rules.py +21 -1
- tests/phase10/benchmark_runner.py +634 -0
- tests/phase10/gold_datasets/entities.json +32 -0
- tests/phase10/gold_datasets/grammar.json +47 -0
- tests/phase10/gold_datasets/hallucination.json +32 -0
- tests/phase10/gold_datasets/punctuation.json +22 -0
- tests/phase10/gold_datasets/religious.json +32 -0
- tests/phase10/gold_datasets/spelling.json +82 -0
- tests/phase10/gold_datasets/structured_content.json +37 -0
- tests/phase10/reports/Phase10_Benchmark_Report.md +214 -0
- tests/phase10/reports/Phase10_Regression_Report.md +119 -0
- tests/phase10/reports/Phase10_RootCause_Report.md +216 -0
- tests/phase10/reports/phase10_results.json +0 -0
- tests/phase8_adversarial_audit.py +678 -0
- tests/phase9_results.json +0 -0
- tests/phase9_validation.py +811 -0
docs/Chapter_1_Introduction.md
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Chapter 1: Introduction
|
| 2 |
+
|
| 3 |
+
## 1.1 Background and Context
|
| 4 |
+
|
| 5 |
+
The Arabic language is the fifth most spoken language globally, with over 420 million speakers across the Middle East and North Africa. As one of the six official languages of the United Nations, Arabic occupies a critical position in global communication, education, governance, and commerce. Despite this prominence, Arabic remains severely underserved by modern Natural Language Processing (NLP) tools when compared to English, Chinese, and European languages. The reasons for this disparity are deeply rooted in the morphological, syntactic, and orthographic complexity inherent to the Arabic writing system.
|
| 6 |
+
|
| 7 |
+
Arabic script is cursive, right-to-left, and context-dependent — the visual form of each character changes based on its position within a word (initial, medial, final, or isolated). Arabic morphology is predominantly non-concatenative: words are constructed by interleaving root consonants with vowel patterns and affixes, producing a combinatorial explosion of surface forms from a single three- or four-letter root. For instance, the root ك-ت-ب (k-t-b, meaning "write") gives rise to كتاب (kitāb, "book"), كاتب (kātib, "writer"), مكتبة (maktaba, "library"), كتابة (kitāba, "writing"), and dozens of additional derivations. This rich morphological system renders simple dictionary-lookup approaches inadequate for spelling correction, grammar checking, and text generation tasks.
|
| 8 |
+
|
| 9 |
+
Furthermore, Arabic text in everyday digital communication is frequently written without diacritical marks (tashkīl), which disambiguate vowel sounds and grammatical case endings. The absence of diacritics creates massive ambiguity: a single unvoweled string may correspond to multiple words with entirely different meanings. For example, the string علم without diacritics can mean "science" (ʿilm), "flag" (ʿalam), "taught" (ʿallama), or "knew" (ʿalima), among others. This pervasive ambiguity compounds the challenge of automated text processing.
|
| 10 |
+
|
| 11 |
+
The commercial landscape for Arabic writing assistance is strikingly barren. Grammarly, the dominant English-language writing assistant with over 30 million daily active users, offers no Arabic support whatsoever. QuillBot provides only basic paraphrasing for Arabic through machine translation proxies, with no grammar checking, spell checking, or punctuation restoration. Microsoft Word's Arabic spell checker relies on a static dictionary compiled decades ago and misses the vast majority of modern Arabic vocabulary, dialectal expressions, and morphological variations. Google Docs provides rudimentary suggestions but lacks the depth of analysis required for formal Arabic writing.
|
| 12 |
+
|
| 13 |
+
This technological gap has tangible consequences. Students submitting academic papers in Arabic must rely on manual proofreading. Journalists and content creators publishing Arabic-language articles have no automated quality assurance tools. Government agencies issuing official Arabic documents lack the writing assistance infrastructure that their English-language counterparts take for granted. The absence of comprehensive Arabic NLP tools is not merely an inconvenience — it is a barrier to the full participation of Arabic-speaking populations in the digital knowledge economy.
|
| 14 |
+
|
| 15 |
+
## 1.2 Problem Statement
|
| 16 |
+
|
| 17 |
+
The core problem addressed by this project can be stated as follows:
|
| 18 |
+
|
| 19 |
+
> **There exists no comprehensive, production-ready, AI-powered writing assistant for the Arabic language that provides integrated spelling correction, grammar checking, punctuation restoration, text summarization, dialect-to-MSA conversion, autocomplete, and Quranic text verification within a unified platform accessible from both a web interface and a browser extension.**
|
| 20 |
+
|
| 21 |
+
This problem decomposes into several interrelated sub-problems:
|
| 22 |
+
|
| 23 |
+
1. **Spelling Correction for Arabic**: Existing Arabic spell checkers are dictionary-based and cannot handle the combinatorial explosion of Arabic morphology. They fail on cliticized forms (e.g., وبالمدرسة — "and in the school"), produce false positives on valid but rare vocabulary, and cannot distinguish between orthographically similar words with different meanings (e.g., كان vs. كأن — "was" vs. "as if").
|
| 24 |
+
|
| 25 |
+
2. **Grammar Checking for Arabic**: Arabic grammar (نحو — naḥw) is governed by a complex system of case endings (إعراب — iʿrāb), number-gender agreement, verb conjugation paradigms (ماضي، مضارع، أمر), and syntactic structures (VSO vs. SVO word order) that existing tools cannot verify or correct.
|
| 26 |
+
|
| 27 |
+
3. **Punctuation Restoration**: Arabic text, particularly in informal digital communication, is frequently written with minimal or no punctuation. Restoring appropriate punctuation (periods, commas, semicolons, question marks in both Arabic and Latin forms) requires understanding sentence boundaries and rhetorical structure.
|
| 28 |
+
|
| 29 |
+
4. **Text Summarization**: Abstractive summarization of Arabic text requires models trained specifically on Arabic corpora, as cross-lingual transfer from English models produces hallucinated content and grammatically incorrect Arabic output.
|
| 30 |
+
|
| 31 |
+
5. **Dialect Handling**: The Arabic-speaking world encompasses a spectrum of dialects (Egyptian, Gulf, Levantine, Maghrebi) that differ substantially from Modern Standard Arabic (MSA — الفصحى). Users who write in dialect require tools to convert their text to formal MSA for academic, professional, and official contexts.
|
| 32 |
+
|
| 33 |
+
6. **Autocomplete**: Predictive text input for Arabic must account for the language's agglutinative tendencies, where prefixes (prepositions, conjunctions, articles) are attached directly to base words.
|
| 34 |
+
|
| 35 |
+
7. **Quranic Text Verification**: Given the centrality of the Quran in Arabic-language writing, the ability to verify and cross-reference Quranic quotations against the canonical text is a unique and valuable feature for Arabic writing tools.
|
| 36 |
+
|
| 37 |
+
## 1.3 Project Objectives
|
| 38 |
+
|
| 39 |
+
The Bayan (بيان) project was conceived to address the problems enumerated above through the design, implementation, and deployment of an integrated Arabic writing assistance platform. The specific objectives of the project are:
|
| 40 |
+
|
| 41 |
+
1. **Design and train a custom Arabic spelling correction model** (AraSpell) based on the AraBERT Encoder-Decoder architecture, incorporating a multi-stage correction pipeline with preprocessing, model inference, re-ranking, contextual refinement, and post-processing.
|
| 42 |
+
|
| 43 |
+
2. **Design and train a custom Arabic punctuation restoration model** (PuncAra-v1) based on a sequence-to-sequence Encoder-Decoder architecture fine-tuned on Arabic text with and without punctuation marks.
|
| 44 |
+
|
| 45 |
+
3. **Integrate a fine-tuned Arabic grammar correction model** using the Gemma 3 architecture, deployed as a Gradio-hosted inference endpoint with rule-based post-processing via CAMeL Tools morphological analysis.
|
| 46 |
+
|
| 47 |
+
4. **Train and deploy an Arabic text summarization model** based on the mBART (Multilingual BART) architecture, fine-tuned on Arabic summarization corpora.
|
| 48 |
+
|
| 49 |
+
5. **Develop a dialect-to-MSA conversion model** using the mT5 (Multilingual T5) architecture, capable of converting Egyptian, Gulf, Levantine, and Maghrebi dialects to formal Modern Standard Arabic.
|
| 50 |
+
|
| 51 |
+
6. **Implement a hybrid Arabic autocomplete system** combining statistical bigram models with neural GPT-2-based contextual prediction (AraGPT2-Base).
|
| 52 |
+
|
| 53 |
+
7. **Build a Quranic text verification engine** backed by a comprehensive SQLite database of the complete Quran with translations, enabling fuzzy search, verse identification, and cross-referencing.
|
| 54 |
+
|
| 55 |
+
8. **Develop a full-stack web application** with a Flask/Gunicorn backend, a rich single-page HTML/CSS/JavaScript frontend featuring a WYSIWYG editor, theme support, and real-time analysis.
|
| 56 |
+
|
| 57 |
+
9. **Develop a Chrome browser extension** (Manifest V3) providing Grammarly-style inline analysis on any web page, with a popup UI, side panel, context menu integration, floating action button, and suggestion tooltips.
|
| 58 |
+
|
| 59 |
+
10. **Deploy the system to production** on HuggingFace Spaces using Docker containerization, with pre-cached models, graceful degradation, and health monitoring.
|
| 60 |
+
|
| 61 |
+
## 1.4 Scope and Delimitations
|
| 62 |
+
|
| 63 |
+
### 1.4.1 In Scope
|
| 64 |
+
|
| 65 |
+
- Arabic text processing: spelling, grammar, punctuation, summarization, dialect conversion, autocomplete, and Quranic verification.
|
| 66 |
+
- Web application with a full-featured editor interface.
|
| 67 |
+
- Chrome browser extension with inline analysis capabilities.
|
| 68 |
+
- Cloud deployment on HuggingFace Spaces.
|
| 69 |
+
- User authentication via Supabase (PostgreSQL-based backend-as-a-service).
|
| 70 |
+
- Document management (create, save, load, cloud sync).
|
| 71 |
+
- Light and dark theme support.
|
| 72 |
+
|
| 73 |
+
### 1.4.2 Out of Scope
|
| 74 |
+
|
| 75 |
+
- Support for languages other than Arabic.
|
| 76 |
+
- Support for browsers other than Chromium-based browsers (Firefox, Safari).
|
| 77 |
+
- Mobile native applications (iOS, Android).
|
| 78 |
+
- Real-time collaborative editing (Google Docs-style multi-cursor).
|
| 79 |
+
- Diacritization (tashkīl) generation — the system processes unvoweled text.
|
| 80 |
+
- Handwriting recognition or OCR-based input.
|
| 81 |
+
- Commercial deployment or monetization infrastructure.
|
| 82 |
+
|
| 83 |
+
## 1.5 Methodology
|
| 84 |
+
|
| 85 |
+
The project followed an iterative, phased development methodology combining elements of Agile sprint planning with a waterfall-style sequential delivery of major system components. The development was organized into the following phases:
|
| 86 |
+
|
| 87 |
+
### Phase 1: Core NLP Pipeline
|
| 88 |
+
Development of the foundational NLP models and services: summarization, spelling correction (AraSpell), grammar correction, and punctuation restoration (PuncAra). Each model was trained independently, evaluated on held-out test sets, and integrated into a unified Flask API backend.
|
| 89 |
+
|
| 90 |
+
### Phase 2: Web Application (Frontend)
|
| 91 |
+
Design and implementation of the single-page web application (SPA) featuring a rich text editor, theme engine, formatting toolbar, real-time analysis display, and document management capabilities.
|
| 92 |
+
|
| 93 |
+
### Phase 3: Feature Expansion
|
| 94 |
+
Addition of autocomplete (hybrid bigram + GPT-2), dialect-to-MSA conversion (mT5), and Quranic text verification. Integration of Supabase for user authentication and cloud document storage.
|
| 95 |
+
|
| 96 |
+
### Phase 4: Chrome Extension — Popup and Context Menu
|
| 97 |
+
Development of the Chrome Manifest V3 extension with a popup UI that mirrors the web application's correction capabilities, plus context menu integration for right-click analysis of selected text on any web page.
|
| 98 |
+
|
| 99 |
+
### Phase 5: Chrome Extension — Side Panel
|
| 100 |
+
Implementation of a persistent side panel (using Chrome's Side Panel API, available since Chrome 114) providing a non-modal, always-available interface for text analysis alongside browsing.
|
| 101 |
+
|
| 102 |
+
### Phase 6: Chrome Extension — Inline Analysis Engine
|
| 103 |
+
Development of a Grammarly-style inline analysis system that highlights errors directly in editable text fields on any web page, with floating tooltips for individual suggestion acceptance.
|
| 104 |
+
|
| 105 |
+
### Phase 7: Production Hardening and Stabilization
|
| 106 |
+
Comprehensive architectural audit, elimination of duplicated infrastructure (cache, retry, hash, API URL, and versioning systems), memory leak fixes, race condition resolution, and production deployment optimization. This phase reduced the codebase by 458 lines while maintaining 100% test pass rate (49/49 unit tests + E2E tests).
|
| 107 |
+
|
| 108 |
+
## 1.6 Tools and Technologies
|
| 109 |
+
|
| 110 |
+
The following tools and technologies were employed in the development of the Bayan system:
|
| 111 |
+
|
| 112 |
+
### 1.6.1 Machine Learning and NLP
|
| 113 |
+
|
| 114 |
+
| Component | Technology | Purpose |
|
| 115 |
+
|---|---|---|
|
| 116 |
+
| Summarization | mBART (MBartForConditionalGeneration) | Arabic text summarization |
|
| 117 |
+
| Spelling | AraBERT Encoder-Decoder + AraSpell Pipeline | Arabic spelling correction |
|
| 118 |
+
| Grammar | Gemma 3 (AutoModelForCausalLM) + CAMeL Tools | Grammar error correction |
|
| 119 |
+
| Punctuation | PuncAra-v1 (EncoderDecoderModel) | Punctuation restoration |
|
| 120 |
+
| Autocomplete | AraGPT2-Base + Bigram Statistical Model | Next-word prediction |
|
| 121 |
+
| Dialect | mT5 (AutoModelForSeq2SeqLM) | Dialect-to-MSA conversion |
|
| 122 |
+
| Tokenization | AraBERT Tokenizer (aubmindlab/bert-base-arabertv02) | Subword tokenization |
|
| 123 |
+
| Morphology | CAMeL Tools MLE Disambiguator | Morphological analysis |
|
| 124 |
+
| Distance Metrics | Levenshtein, Damerau-Levenshtein, Jellyfish | Edit distance computation |
|
| 125 |
+
|
| 126 |
+
### 1.6.2 Backend
|
| 127 |
+
|
| 128 |
+
| Component | Technology | Version |
|
| 129 |
+
|---|---|---|
|
| 130 |
+
| Web Framework | Flask | Latest |
|
| 131 |
+
| CORS | Flask-CORS | Latest |
|
| 132 |
+
| WSGI Server | Gunicorn | Latest |
|
| 133 |
+
| ML Framework | PyTorch | Latest |
|
| 134 |
+
| Model Hub | HuggingFace Transformers + Hub | ≥0.20.0 |
|
| 135 |
+
| Inference Proxy | Gradio Client | Latest |
|
| 136 |
+
| Environment | python-dotenv | Latest |
|
| 137 |
+
| String Matching | RapidFuzz | Latest |
|
| 138 |
+
|
| 139 |
+
### 1.6.3 Frontend
|
| 140 |
+
|
| 141 |
+
| Component | Technology |
|
| 142 |
+
|---|---|
|
| 143 |
+
| Markup | HTML5 |
|
| 144 |
+
| Styling | CSS3 (Vanilla, CSS Variables) |
|
| 145 |
+
| Logic | Vanilla JavaScript (ES6+) |
|
| 146 |
+
| Authentication | Supabase JS Client |
|
| 147 |
+
| Editor | Custom contenteditable-based WYSIWYG editor |
|
| 148 |
+
|
| 149 |
+
### 1.6.4 Chrome Extension
|
| 150 |
+
|
| 151 |
+
| Component | Technology |
|
| 152 |
+
|---|---|
|
| 153 |
+
| Manifest | Chrome Manifest V3 |
|
| 154 |
+
| Background | Service Worker (background.js) |
|
| 155 |
+
| Content Script | content-inline.js + content-inline.css |
|
| 156 |
+
| Side Panel | Chrome Side Panel API (Chrome ≥114) |
|
| 157 |
+
| Popup | popup.html + popup.js + popup.css |
|
| 158 |
+
| Internationalization | Chrome i18n (_locales/) |
|
| 159 |
+
|
| 160 |
+
### 1.6.5 Infrastructure and Deployment
|
| 161 |
+
|
| 162 |
+
| Component | Technology |
|
| 163 |
+
|---|---|
|
| 164 |
+
| Containerization | Docker (python:3.12-slim) |
|
| 165 |
+
| Cloud Platform | HuggingFace Spaces |
|
| 166 |
+
| Database (Auth) | Supabase (PostgreSQL) |
|
| 167 |
+
| Database (Quran) | SQLite (quran_master.db, ~22MB) |
|
| 168 |
+
| Version Control | Git + GitHub |
|
| 169 |
+
| CI/CD | GitHub Actions |
|
| 170 |
+
|
| 171 |
+
## 1.7 Project Organization
|
| 172 |
+
|
| 173 |
+
### 1.7.1 Team Structure
|
| 174 |
+
|
| 175 |
+
The Bayan project was developed by a team of computer science students as a graduation capstone project, with each member contributing to specific subsystems while collaborating on integration and testing.
|
| 176 |
+
|
| 177 |
+
### 1.7.2 Repository Structure
|
| 178 |
+
|
| 179 |
+
The project repository is organized as follows:
|
| 180 |
+
|
| 181 |
+
```
|
| 182 |
+
BAYAN/
|
| 183 |
+
├── src/ # Backend application
|
| 184 |
+
│ ├── app.py # Flask API server (1,717 lines)
|
| 185 |
+
│ ├── model_loader.py # Model loading and inference (904 lines)
|
| 186 |
+
│ ├── hf_inference.py # HuggingFace API fallback
|
| 187 |
+
│ ├── index.html # Web application frontend
|
| 188 |
+
│ ├── js/ # Frontend JavaScript modules
|
| 189 |
+
│ │ ├── editor.js # WYSIWYG editor logic
|
| 190 |
+
│ │ ├── renderer.js # Analysis result rendering
|
| 191 |
+
│ │ ├── autocomplete.js # Autocomplete UI
|
| 192 |
+
│ │ ├── ui.js # General UI management
|
| 193 |
+
│ │ ├── api.js # API client
|
| 194 |
+
│ │ ├── format.js # Text formatting toolbar
|
| 195 |
+
│ │ ├── selection.js # Text selection handling
|
| 196 |
+
│ │ ├── theme.js # Theme management
|
| 197 |
+
│ │ ├── auth/ # Authentication module
|
| 198 |
+
│ │ ├── documents/ # Document management
|
| 199 |
+
│ │ ├── documents-cloud/ # Cloud document sync
|
| 200 |
+
│ │ ├── summaries/ # Summary display
|
| 201 |
+
│ │ └── vendor/ # Third-party libraries
|
| 202 |
+
│ ├── css/ # Stylesheets
|
| 203 |
+
│ └── nlp/ # NLP Pipeline modules
|
| 204 |
+
│ ├── pipeline_context.py # Pipeline state management
|
| 205 |
+
│ ├── correction_patch.py # Patch/suggestion data model
|
| 206 |
+
│ ├── stage_locker.py # Cross-stage conflict resolution
|
| 207 |
+
│ ├── spelling/
|
| 208 |
+
│ │ ├── araspell_rules.py # AraSpell pipeline (1,507 lines)
|
| 209 |
+
│ │ └── araspell_service.py
|
| 210 |
+
│ ├── grammar/
|
| 211 |
+
│ │ ├── grammar_rules.py # CAMeL Tools rules (294 lines)
|
| 212 |
+
│ │ └── grammar_service.py
|
| 213 |
+
│ ├── punctuation/
|
| 214 |
+
│ │ ├── punctuation_rules.py
|
| 215 |
+
│ │ └── punctuation_service.py
|
| 216 |
+
│ ├── autocomplete/
|
| 217 |
+
│ │ ├── autocomplete_rules.py
|
| 218 |
+
│ │ └── autocomplete_service.py
|
| 219 |
+
│ └── dialect/
|
| 220 |
+
│ └── dialect_service.py
|
| 221 |
+
├── extension/ # Chrome Extension
|
| 222 |
+
│ ├── manifest.json # Manifest V3 configuration
|
| 223 |
+
│ ├── background.js # Service worker
|
| 224 |
+
│ ├── content-inline.js # Inline analysis engine
|
| 225 |
+
│ ├── content-inline.css # Inline analysis styles
|
| 226 |
+
│ ├── popup.html / .js / .css # Popup UI
|
| 227 |
+
│ ├── sidepanel/ # Side panel UI
|
| 228 |
+
│ ├── shared/ # Shared extension modules
|
| 229 |
+
│ │ ├── analysis-controller.js
|
| 230 |
+
│ │ ├── bayan-api.js
|
| 231 |
+
│ │ ├── bayan-patches.js
|
| 232 |
+
│ │ ├── bayan-renderer.js
|
| 233 |
+
│ │ ├── bayan-state.js
|
| 234 |
+
│ │ ├── bayan-ui.js
|
| 235 |
+
│ │ ├── config.js
|
| 236 |
+
│ │ ├── constants.js
|
| 237 |
+
│ │ └── hash.js
|
| 238 |
+
│ └── tests/ # Extension tests
|
| 239 |
+
├── quran.py # Quran search engine
|
| 240 |
+
├── quran_master.db # Quran SQLite database (~22MB)
|
| 241 |
+
├── Dockerfile # Production container
|
| 242 |
+
├── requirements.txt # Python dependencies
|
| 243 |
+
├── tests/ # Unit and integration tests
|
| 244 |
+
└── docs/ # Documentation
|
| 245 |
+
```
|
| 246 |
+
|
| 247 |
+
### 1.7.3 Development Timeline (Gantt Chart)
|
| 248 |
+
|
| 249 |
+
```mermaid
|
| 250 |
+
gantt
|
| 251 |
+
title Bayan Development Phases
|
| 252 |
+
dateFormat YYYY-MM
|
| 253 |
+
axisFormat %b %Y
|
| 254 |
+
|
| 255 |
+
section Phase 1
|
| 256 |
+
NLP Pipeline Development :p1, 2025-09, 3M
|
| 257 |
+
Model Training & Evaluation :p1b, 2025-10, 2M
|
| 258 |
+
|
| 259 |
+
section Phase 2
|
| 260 |
+
Web App Frontend :p2, 2025-12, 2M
|
| 261 |
+
|
| 262 |
+
section Phase 3
|
| 263 |
+
Feature Expansion :p3, 2026-02, 2M
|
| 264 |
+
Autocomplete + Dialect + Quran :p3b, 2026-03, 1M
|
| 265 |
+
|
| 266 |
+
section Phase 4
|
| 267 |
+
Chrome Extension Popup :p4, 2026-04, 1M
|
| 268 |
+
|
| 269 |
+
section Phase 5
|
| 270 |
+
Side Panel :p5, 2026-04, 1M
|
| 271 |
+
|
| 272 |
+
section Phase 6
|
| 273 |
+
Inline Analysis Engine :p6, 2026-05, 1M
|
| 274 |
+
|
| 275 |
+
section Phase 7
|
| 276 |
+
Production Hardening :p7, 2026-06, 1M
|
| 277 |
+
Stabilization Sprint :p7b, 2026-06, 1M
|
| 278 |
+
```
|
| 279 |
+
|
| 280 |
+
## 1.8 Report Organization
|
| 281 |
+
|
| 282 |
+
This report is organized into seven chapters:
|
| 283 |
+
|
| 284 |
+
- **Chapter 1 (Introduction)**: Provides background, problem statement, objectives, scope, methodology, tools, and project organization.
|
| 285 |
+
- **Chapter 2 (Literature Review)**: Surveys related work in Arabic NLP, spell checking, grammar correction, summarization, and writing assistance tools.
|
| 286 |
+
- **Chapter 3 (System Design and Architecture)**: Presents the overall system architecture, backend API design, NLP pipeline architecture, frontend design, and Chrome extension architecture.
|
| 287 |
+
- **Chapter 4 (Implementation)**: Details the implementation of each system component, including model training, API development, frontend features, and extension mechanics.
|
| 288 |
+
- **Chapter 5 (Testing and Evaluation)**: Describes the testing methodology, test results, model evaluation metrics, and production hardening audit findings.
|
| 289 |
+
- **Chapter 6 (Results and Discussion)**: Presents the system's capabilities, performance benchmarks, competitive analysis against Grammarly and QuillBot, and discusses limitations.
|
| 290 |
+
- **Chapter 7 (Conclusion and Future Work)**: Summarizes contributions, reflects on lessons learned, and outlines a roadmap for future development.
|
docs/Chapter_2_Literature_Review.md
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Chapter 2: Literature Review
|
| 2 |
+
|
| 3 |
+
## 2.1 Overview
|
| 4 |
+
|
| 5 |
+
This chapter surveys the existing body of research and commercial tools relevant to the Bayan project. The review covers five major domains: Arabic Natural Language Processing (NLP) fundamentals, Arabic spell checking and grammar correction, Arabic text summarization, writing assistance tools, and browser extension technologies. For each domain, we examine the state of the art, identify key limitations, and position the contributions of the Bayan system within the broader landscape.
|
| 6 |
+
|
| 7 |
+
## 2.2 Arabic Natural Language Processing
|
| 8 |
+
|
| 9 |
+
### 2.2.1 Challenges of Arabic NLP
|
| 10 |
+
|
| 11 |
+
Arabic presents a unique constellation of challenges for NLP systems, documented extensively in the literature (Habash, 2010; Farghaly & Shaalan, 2009). These challenges include:
|
| 12 |
+
|
| 13 |
+
**Morphological Complexity**: Arabic is a morphologically rich language with a root-and-pattern system. A typical Arabic root consists of three consonants (triliteral root, e.g., ك-ت-ب) from which dozens of surface forms are derived through the insertion of vowel patterns, prefixes, suffixes, and infixes. Habash (2010) catalogued over 60 distinct morphological features that can be expressed through affixation and templatic morphology, producing an estimated 72 billion possible word forms from approximately 10,000 root entries.
|
| 14 |
+
|
| 15 |
+
**Orthographic Ambiguity**: The Arabic writing system omits short vowels (diacritics/tashkīl) in most contexts, creating pervasive lexical and syntactic ambiguity. Diab et al. (2007) demonstrated that a single Arabic word form may have an average of 12 possible analyses, compared to 2.4 for English. This ambiguity fundamentally impacts downstream NLP tasks including tokenization, part-of-speech tagging, and named entity recognition.
|
| 16 |
+
|
| 17 |
+
**Dialectal Variation**: Modern Standard Arabic (MSA) coexists with a spectrum of regional dialects (Egyptian, Gulf, Levantine, Maghrebi, and others) that differ substantially in vocabulary, phonology, and syntax. Bouamor et al. (2018) showed that inter-dialect intelligibility can be as low as 60% for geographically distant varieties. Most NLP tools are trained exclusively on MSA, leaving dialectal text inadequately served.
|
| 18 |
+
|
| 19 |
+
**Hamza and Ta Marbuta Confusion**: Two of the most frequent orthographic errors in Arabic involve hamza placement (أ/إ/آ/ء/ئ/ؤ) and the confusion between ta marbuta (ة) and ha (ه) at word endings. Zaghouani et al. (2014) found that these two error categories account for over 35% of all spelling errors in Arabic text.
|
| 20 |
+
|
| 21 |
+
### 2.2.2 Pre-trained Arabic Language Models
|
| 22 |
+
|
| 23 |
+
The development of pre-trained language models specifically for Arabic has been a critical enabler for downstream NLP tasks. Key models include:
|
| 24 |
+
|
| 25 |
+
**AraBERT** (Antoun et al., 2020): A BERT-based model pre-trained on 70 million Arabic sentences from news articles, Wikipedia, and OSCAR datasets. AraBERT introduced Arabic-specific preprocessing (Farasa segmentation, removing diacritics and tatweel) and achieved state-of-the-art results on several Arabic NLP benchmarks. The Bayan project uses AraBERT (aubmindlab/bert-base-arabertv02) as the encoder-decoder backbone for the AraSpell spelling correction model.
|
| 26 |
+
|
| 27 |
+
**AraGPT2** (Antoun et al., 2021): An Arabic adaptation of GPT-2, pre-trained on a large Arabic corpus for autoregressive language modeling. Available in base, medium, and large variants. The Bayan project uses AraGPT2-Base (aubmindlab/aragpt2-base) as the neural component of the hybrid autocomplete system.
|
| 28 |
+
|
| 29 |
+
**CAMeL Tools** (Obeid et al., 2020): A comprehensive Arabic NLP toolkit providing morphological analysis, disambiguation, and dialectal identification. The MLE (Maximum Likelihood Estimation) disambiguator from CAMeL Tools is used in Bayan's grammar post-processing rules to determine part-of-speech tags, number, and gender for agreement checking.
|
| 30 |
+
|
| 31 |
+
**mBART** (Liu et al., 2020): A multilingual denoising autoencoder pre-trained on 25 languages including Arabic. mBART's sequence-to-sequence architecture makes it suitable for text generation tasks. Bayan uses a fine-tuned mBART model (MBartForConditionalGeneration) for Arabic text summarization.
|
| 32 |
+
|
| 33 |
+
**mT5** (Xue et al., 2021): A multilingual variant of the Text-to-Text Transfer Transformer (T5) pre-trained on 101 languages. Bayan uses a fine-tuned mT5 model (AutoModelForSeq2SeqLM) for dialect-to-MSA conversion.
|
| 34 |
+
|
| 35 |
+
## 2.3 Arabic Spelling Correction
|
| 36 |
+
|
| 37 |
+
### 2.3.1 Traditional Approaches
|
| 38 |
+
|
| 39 |
+
Early Arabic spell checkers relied on dictionary lookup combined with edit distance metrics. Haddad and Yaseen (2007) proposed an Arabic spell checker using minimum edit distance with a dictionary of 9 million word forms. Ben Othmane Zribi and Ben Ahmed (2003) developed a morphological analyzer-based approach that decomposed words into roots, patterns, prefixes, and suffixes before dictionary validation.
|
| 40 |
+
|
| 41 |
+
These approaches suffer from two fundamental limitations: (1) the dictionary can never be complete due to Arabic's productive morphology, and (2) edit distance metrics designed for English do not account for Arabic-specific error patterns such as hamza misplacement, ta marbuta/ha confusion, and clitic attachment errors.
|
| 42 |
+
|
| 43 |
+
### 2.3.2 Neural Approaches
|
| 44 |
+
|
| 45 |
+
Recent work has applied sequence-to-sequence models to Arabic spell checking:
|
| 46 |
+
|
| 47 |
+
**Mohit et al. (2014)** proposed one of the first neural approaches to Arabic text correction, using statistical machine translation (SMT) techniques to "translate" erroneous text into correct text.
|
| 48 |
+
|
| 49 |
+
**Watson et al. (2019)** demonstrated that transformer-based models could achieve significant improvements over traditional approaches for Arabic grammatical error correction, particularly for hamza and morphological agreement errors.
|
| 50 |
+
|
| 51 |
+
**AraSpell (Bayan)**: The spelling correction system developed in this project represents a novel contribution: a full pipeline integrating rule-based preprocessing, neural correction (AraBERT Encoder-Decoder), hybrid word alignment, contextual refinement using masked language modeling (BERT MLM), and extensive post-processing with vocabulary validation. This multi-stage approach addresses the over-correction problem that plagues single-model approaches, where the model aggressively changes valid but rare words to more common alternatives.
|
| 52 |
+
|
| 53 |
+
### 2.3.3 Key Challenges in Arabic Spell Checking
|
| 54 |
+
|
| 55 |
+
The AraSpell development process identified and addressed several challenges not adequately covered in the literature:
|
| 56 |
+
|
| 57 |
+
1. **In-Vocabulary (IV) to In-Vocabulary (IV) Corruption**: When both the original and corrected words are valid Arabic words, the model may change the meaning (e.g., كان → كأن, "was" → "as if"). This requires vocabulary-aware filtering.
|
| 58 |
+
|
| 59 |
+
2. **Pronoun Suffix False Positives**: The ه→ة correction (ha to ta marbuta) is generally correct for feminine nouns but incorrect when ه is a pronoun suffix (e.g., فتأملته — "she contemplated him").
|
| 60 |
+
|
| 61 |
+
3. **Numeral Hallucination**: Neural models occasionally introduce or modify digits in the output, a complete-replacement failure mode.
|
| 62 |
+
|
| 63 |
+
4. **Word Split Validation**: Merged words (e.g., فيالمدرسة → في المدرسة) must be split carefully to avoid detaching pronoun suffixes (e.g., مستشفياتهم → "مستشفيات هم" is incorrect).
|
| 64 |
+
|
| 65 |
+
## 2.4 Arabic Grammar Correction
|
| 66 |
+
|
| 67 |
+
### 2.4.1 Rule-Based Approaches
|
| 68 |
+
|
| 69 |
+
Traditional Arabic grammar checking relies on morphological analysis and hand-crafted rules. Shaalan et al. (2012) developed a rule-based Arabic grammar checker that addressed subject-verb agreement, definiteness agreement, and case marking errors using a morphological analyzer. However, rule-based systems are inherently limited by the rules they encode and cannot generalize to unseen error patterns.
|
| 70 |
+
|
| 71 |
+
### 2.4.2 Neural Approaches
|
| 72 |
+
|
| 73 |
+
**Soliman et al. (2017)** applied sequence-to-sequence models to Arabic GEC (Grammatical Error Correction), achieving moderate improvements over rule-based baselines.
|
| 74 |
+
|
| 75 |
+
**Gemma (Google DeepMind, 2024)**: The Gemma family of language models, based on the research behind Google's Gemini models, provides instruction-following capabilities that can be adapted for GEC tasks through fine-tuning. Bayan uses a fine-tuned Gemma 3 model (AutoModelForCausalLM) deployed as a Gradio-hosted inference endpoint.
|
| 76 |
+
|
| 77 |
+
### 2.4.3 Hybrid Approaches
|
| 78 |
+
|
| 79 |
+
The Bayan grammar correction system employs a hybrid approach: neural model inference (Gemma 3 via Gradio) followed by rule-based post-processing using CAMeL Tools. The rule-based component addresses:
|
| 80 |
+
|
| 81 |
+
- **Number and Gender Agreement**: Ensuring adjectives, verbs, and demonstratives agree with their governing nouns in number (singular/dual/plural) and gender (masculine/feminine).
|
| 82 |
+
- **Case Marking with Prepositions**: Converting nominative endings to genitive after prepositions (e.g., المهندسون → المهندسين after في).
|
| 83 |
+
- **The Five Nouns (الأسماء الخمسة)**: Special declension rules for أب (father), أخ (brother), حم (father-in-law), فو (mouth), ذو (possessor of).
|
| 84 |
+
- **Nasb and Jazm of Verbs**: Correct verb endings after particles of subjunctive (أن, لن, كي) and jussive (لم, لا, لمّا).
|
| 85 |
+
- **Subject-Verb Agreement in SVO Order**: In Arabic, VSO (Verb-Subject-Object) order allows a singular verb with a plural subject, but SVO order requires number agreement.
|
| 86 |
+
|
| 87 |
+
## 2.5 Arabic Text Summarization
|
| 88 |
+
|
| 89 |
+
### 2.5.1 Extractive vs. Abstractive Summarization
|
| 90 |
+
|
| 91 |
+
Summarization techniques fall into two categories:
|
| 92 |
+
|
| 93 |
+
**Extractive summarization** selects and concatenates the most important sentences from the source text. Al-Sabahi et al. (2018) surveyed extractive methods for Arabic, including TF-IDF-based, graph-based (TextRank), and topic modeling approaches. While reliable, extractive methods produce summaries that lack coherence and may miss implicit information.
|
| 94 |
+
|
| 95 |
+
**Abstractive summarization** generates new text that captures the meaning of the source document. This is significantly more challenging as it requires natural language generation. Al-Maleh and Deris (2020) demonstrated that transformer-based models (mBART, mT5) could produce coherent Arabic summaries when fine-tuned on Arabic summarization datasets.
|
| 96 |
+
|
| 97 |
+
### 2.5.2 Bayan's Approach
|
| 98 |
+
|
| 99 |
+
Bayan's summarization system uses a fine-tuned mBART model with the following design decisions:
|
| 100 |
+
|
| 101 |
+
- **Greedy decoding** (num_beams=1, do_sample=False) was empirically found to produce more faithful summaries than beam search, which tended to generate generic or hallucinated content.
|
| 102 |
+
- **Extractive fallback**: When the model's output has low lexical overlap with the source (overlap ratio < 0.35 or SequenceMatcher ratio < 0.22), the system falls back to an extractive approach, selecting the opening sentences of the source text.
|
| 103 |
+
- **Configurable summary length**: Three tiers (short ~30%, medium ~50%, long ~70% of input length) allow users to control the compression ratio.
|
| 104 |
+
|
| 105 |
+
## 2.6 Arabic Punctuation Restoration
|
| 106 |
+
|
| 107 |
+
### 2.6.1 Background
|
| 108 |
+
|
| 109 |
+
Arabic text, particularly in informal digital communication, frequently lacks proper punctuation. Punctuation restoration (also called punctuation prediction) is the task of inserting appropriate punctuation marks into unpunctuated text. Che et al. (2016) formulated this as a sequence labeling problem, where each word is classified as having a punctuation mark after it (and which mark) or not.
|
| 110 |
+
|
| 111 |
+
### 2.6.2 Bayan's PuncAra Model
|
| 112 |
+
|
| 113 |
+
The PuncAra-v1 model developed for this project is a sequence-to-sequence EncoderDecoderModel fine-tuned on Arabic text with and without punctuation. Key design features include:
|
| 114 |
+
|
| 115 |
+
- **Windowed chunking**: Long texts are processed in 50-word non-overlapping windows, with trailing punctuation removed from non-final segments to avoid false sentence boundaries at chunk edges.
|
| 116 |
+
- **Non-punctuation change stripping**: The model was trained on data that included spelling/grammar corrections alongside punctuation. A post-processing step (Fix P1) strips any changes to word content, preserving only punctuation additions/modifications.
|
| 117 |
+
- **Punctuation-only diff validation**: A safety layer validates that each diff produced by the punctuation stage only adds or modifies punctuation characters, rejecting any diffs that alter Arabic word content.
|
| 118 |
+
|
| 119 |
+
## 2.7 Writing Assistance Tools
|
| 120 |
+
|
| 121 |
+
### 2.7.1 Grammarly
|
| 122 |
+
|
| 123 |
+
Grammarly (founded 2009) is the market leader in English writing assistance, with over 30 million daily active users. Its feature set includes:
|
| 124 |
+
|
| 125 |
+
- Real-time grammar, spelling, and punctuation checking
|
| 126 |
+
- Tone detection and style suggestions
|
| 127 |
+
- Plagiarism detection
|
| 128 |
+
- Clarity and engagement scoring
|
| 129 |
+
- Browser extension with inline highlighting and floating cards
|
| 130 |
+
- Desktop application and mobile keyboard
|
| 131 |
+
|
| 132 |
+
Grammarly's architecture combines rule-based NLP with deep learning models, running inference on cloud servers with edge-optimized client-side processing for low-latency suggestions. **Grammarly offers no Arabic language support.**
|
| 133 |
+
|
| 134 |
+
### 2.7.2 QuillBot
|
| 135 |
+
|
| 136 |
+
QuillBot (founded 2017) focuses on paraphrasing and writing assistance:
|
| 137 |
+
|
| 138 |
+
- Paraphraser with multiple modes (standard, fluency, creative, etc.)
|
| 139 |
+
- Grammar checker (English only)
|
| 140 |
+
- Summarizer
|
| 141 |
+
- Citation generator
|
| 142 |
+
- Co-Writer
|
| 143 |
+
|
| 144 |
+
QuillBot's Arabic support is limited to basic paraphrasing through machine translation proxies, with no grammar checking, spell checking, or punctuation restoration for Arabic.
|
| 145 |
+
|
| 146 |
+
### 2.7.3 Bayan's Position
|
| 147 |
+
|
| 148 |
+
Bayan occupies a unique position as the first comprehensive Arabic writing assistant that integrates all major NLP capabilities (spelling, grammar, punctuation, summarization, dialect conversion, autocomplete, and Quranic verification) within a unified platform accessible from both a web interface and a browser extension. The competitive gap analysis conducted during Phase 7 identified 47 features present in Grammarly or QuillBot, of which Bayan implements 22, with the remaining 25 representing future development opportunities.
|
| 149 |
+
|
| 150 |
+
## 2.8 Chrome Extension Technologies
|
| 151 |
+
|
| 152 |
+
### 2.8.1 Manifest V3
|
| 153 |
+
|
| 154 |
+
Google Chrome's Manifest V3 (MV3) extension platform, mandated for all new extensions since January 2023, introduced significant architectural changes from Manifest V2:
|
| 155 |
+
|
| 156 |
+
- **Service Workers** replace persistent background pages, requiring stateless message-passing architectures.
|
| 157 |
+
- **Host Permissions** replace broad `<all_urls>` access with explicit domain declarations.
|
| 158 |
+
- **Content Scripts** operate in an isolated world, communicating with the service worker via `chrome.runtime.sendMessage()`.
|
| 159 |
+
- **Side Panel API** (Chrome ≥ 114) provides a persistent panel alongside the browsing window.
|
| 160 |
+
|
| 161 |
+
### 2.8.2 Grammarly-Style Inline Analysis
|
| 162 |
+
|
| 163 |
+
The Grammarly browser extension pioneered the pattern of inline text analysis on arbitrary web pages, which Bayan's Phase 6 (inline analysis engine) implements. This pattern involves:
|
| 164 |
+
|
| 165 |
+
1. **Content script injection**: Detecting editable text fields (`<textarea>`, `contenteditable`, `<input>`) on any web page.
|
| 166 |
+
2. **Overlay rendering**: Creating a positioned overlay layer that renders colored underlines beneath detected errors, without modifying the underlying DOM content.
|
| 167 |
+
3. **Suggestion tooltips**: Displaying floating cards with correction options when the user hovers over or clicks an underlined error.
|
| 168 |
+
4. **Non-destructive correction**: Applying corrections by modifying the text content of the editable field, preserving cursor position and selection state.
|
| 169 |
+
|
| 170 |
+
### 2.8.3 Challenges of Content Script Architecture
|
| 171 |
+
|
| 172 |
+
Content scripts operating on arbitrary web pages face several challenges that the literature addresses only partially:
|
| 173 |
+
|
| 174 |
+
- **Shadow DOM isolation**: Modern web frameworks (React, Angular, Vue) use Shadow DOM boundaries that prevent content scripts from accessing internal elements.
|
| 175 |
+
- **Dynamic content**: Single-page applications (SPAs) dynamically create and destroy text fields, requiring MutationObserver-based detection.
|
| 176 |
+
- **Protected pages**: Browser-internal pages (`chrome://`, `chrome-extension://`), the Chrome Web Store, and certain Google properties block content script injection entirely.
|
| 177 |
+
- **Performance**: Real-time text analysis with network round-trips to a cloud API must be debounced and throttled to avoid degrading page responsiveness.
|
| 178 |
+
|
| 179 |
+
## 2.9 Summary of Literature Review
|
| 180 |
+
|
| 181 |
+
The literature review reveals a clear gap in the Arabic NLP landscape: while individual components (spell checking, grammar correction, summarization) have been studied in isolation, no existing system integrates these capabilities into a comprehensive, production-ready writing assistant platform. Commercial tools (Grammarly, QuillBot) either do not support Arabic at all or provide only superficial coverage. The Bayan project addresses this gap by combining custom-trained Arabic NLP models with a full-stack web application and a feature-rich Chrome browser extension, creating the first end-to-end Arabic writing assistance system of its kind.
|
| 182 |
+
|
| 183 |
+
| Capability | Grammarly | QuillBot | Bayan |
|
| 184 |
+
|---|---|---|---|
|
| 185 |
+
| Arabic Spelling Correction | ❌ | ❌ | ✅ |
|
| 186 |
+
| Arabic Grammar Checking | ❌ | ❌ | ✅ |
|
| 187 |
+
| Arabic Punctuation Restoration | ❌ | ❌ | ✅ |
|
| 188 |
+
| Arabic Summarization | ❌ | Partial | ✅ |
|
| 189 |
+
| Dialect-to-MSA Conversion | ❌ | ❌ | ✅ |
|
| 190 |
+
| Arabic Autocomplete | ❌ | ❌ | ✅ |
|
| 191 |
+
| Quranic Verification | ❌ | ❌ | ✅ |
|
| 192 |
+
| Browser Extension (Inline) | ✅ (English) | ✅ (English) | ✅ (Arabic) |
|
| 193 |
+
| Side Panel | ❌ | ❌ | ✅ |
|
| 194 |
+
| Web Application | ✅ (English) | ✅ (English) | ✅ (Arabic) |
|
docs/Chapter_3_System_Design.md
ADDED
|
@@ -0,0 +1,655 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Chapter 3: System Design and Architecture
|
| 2 |
+
|
| 3 |
+
## 3.1 Overview
|
| 4 |
+
|
| 5 |
+
The Bayan system is a distributed, multi-tier architecture comprising four principal components: (1) a Python/Flask backend providing RESTful API endpoints for NLP model inference, (2) a single-page web application (SPA) frontend for direct text analysis, (3) a Chrome Manifest V3 browser extension for in-browser writing assistance, and (4) cloud infrastructure for deployment, authentication, and data persistence. This chapter presents the architectural design of each component, the data flow between components, and the key design decisions that shaped the system.
|
| 6 |
+
|
| 7 |
+
## 3.2 High-Level System Architecture
|
| 8 |
+
|
| 9 |
+
```mermaid
|
| 10 |
+
graph TB
|
| 11 |
+
subgraph "Client Layer"
|
| 12 |
+
WEB["Web Application<br/>(index.html + JS modules)"]
|
| 13 |
+
EXT_POPUP["Chrome Extension<br/>Popup UI"]
|
| 14 |
+
EXT_SIDE["Chrome Extension<br/>Side Panel"]
|
| 15 |
+
EXT_INLINE["Chrome Extension<br/>Inline Analysis Engine"]
|
| 16 |
+
end
|
| 17 |
+
|
| 18 |
+
subgraph "Extension Architecture"
|
| 19 |
+
BG["Service Worker<br/>(background.js)"]
|
| 20 |
+
CS["Content Script<br/>(content-inline.js)"]
|
| 21 |
+
SHARED["Shared Modules<br/>(analysis-controller, bayan-api,<br/>bayan-patches, bayan-renderer,<br/>bayan-state, bayan-ui)"]
|
| 22 |
+
end
|
| 23 |
+
|
| 24 |
+
subgraph "Backend Layer"
|
| 25 |
+
FLASK["Flask API Server<br/>(app.py)"]
|
| 26 |
+
subgraph "NLP Pipeline"
|
| 27 |
+
SPELL["Spelling<br/>(AraSpell)"]
|
| 28 |
+
GRAM["Grammar<br/>(Gemma 3 + CAMeL)"]
|
| 29 |
+
PUNC["Punctuation<br/>(PuncAra-v1)"]
|
| 30 |
+
SUMM["Summarization<br/>(mBART)"]
|
| 31 |
+
AUTO["Autocomplete<br/>(Bigram + AraGPT2)"]
|
| 32 |
+
DIAL["Dialect<br/>(mT5)"]
|
| 33 |
+
QURAN["Quran Search<br/>(SQLite)"]
|
| 34 |
+
end
|
| 35 |
+
ML["Model Loader<br/>(model_loader.py)"]
|
| 36 |
+
HF["HF Inference<br/>Fallback"]
|
| 37 |
+
end
|
| 38 |
+
|
| 39 |
+
subgraph "Data Layer"
|
| 40 |
+
SUPA["Supabase<br/>(Auth + Documents)"]
|
| 41 |
+
SQLITE["quran_master.db<br/>(SQLite ~22MB)"]
|
| 42 |
+
HFHUB["HuggingFace Hub<br/>(Model Weights)"]
|
| 43 |
+
end
|
| 44 |
+
|
| 45 |
+
WEB -->|"HTTP/JSON"| FLASK
|
| 46 |
+
EXT_POPUP -->|"chrome.runtime.sendMessage"| BG
|
| 47 |
+
EXT_SIDE -->|"chrome.runtime.sendMessage"| BG
|
| 48 |
+
EXT_INLINE -->|"Content Script"| CS
|
| 49 |
+
CS -->|"chrome.runtime.sendMessage"| BG
|
| 50 |
+
BG -->|"fetch() proxy"| FLASK
|
| 51 |
+
SHARED -.->|"imported by"| EXT_POPUP
|
| 52 |
+
SHARED -.->|"imported by"| EXT_SIDE
|
| 53 |
+
SHARED -.->|"imported by"| CS
|
| 54 |
+
|
| 55 |
+
FLASK --> SPELL
|
| 56 |
+
FLASK --> GRAM
|
| 57 |
+
FLASK --> PUNC
|
| 58 |
+
FLASK --> SUMM
|
| 59 |
+
FLASK --> AUTO
|
| 60 |
+
FLASK --> DIAL
|
| 61 |
+
FLASK --> QURAN
|
| 62 |
+
FLASK --> ML
|
| 63 |
+
FLASK --> HF
|
| 64 |
+
|
| 65 |
+
WEB -->|"Supabase JS Client"| SUPA
|
| 66 |
+
QURAN --> SQLITE
|
| 67 |
+
ML --> HFHUB
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
## 3.3 Backend Architecture
|
| 71 |
+
|
| 72 |
+
### 3.3.1 Flask API Server
|
| 73 |
+
|
| 74 |
+
The backend is a Flask application (`src/app.py`, 1,717 lines) that exposes RESTful API endpoints for all NLP operations. The server is designed to run under Gunicorn with a single worker process to minimize RAM consumption on the free-tier HuggingFace Spaces deployment (16GB RAM limit).
|
| 75 |
+
|
| 76 |
+
**API Endpoints:**
|
| 77 |
+
|
| 78 |
+
| Endpoint | Method | Purpose | Model |
|
| 79 |
+
|---|---|---|---|
|
| 80 |
+
| `/api/health` | GET | Health check and model status | — |
|
| 81 |
+
| `/api/debug-models` | GET | Debug model loading diagnostics | — |
|
| 82 |
+
| `/api/spelling` | POST | Standalone spelling correction | AraSpell |
|
| 83 |
+
| `/api/grammar` | POST | Standalone grammar correction | Gemma 3 |
|
| 84 |
+
| `/api/punctuation` | POST | Standalone punctuation restoration | PuncAra-v1 |
|
| 85 |
+
| `/api/summarize` | POST | Text summarization | mBART |
|
| 86 |
+
| `/api/autocomplete` | POST | Next-word prediction | Bigram + AraGPT2 |
|
| 87 |
+
| `/api/dialect` | POST | Dialect-to-MSA conversion | mT5 |
|
| 88 |
+
| `/api/quran` | POST | Quranic text verification | SQLite |
|
| 89 |
+
| `/api/analyze` | POST | Multi-stage sequential analysis | AraSpell → Gemma 3 → PuncAra |
|
| 90 |
+
|
| 91 |
+
### 3.3.2 Model Loading Strategy
|
| 92 |
+
|
| 93 |
+
The model loading strategy (`src/model_loader.py`, 904 lines) is designed to handle the constraints of the free-tier deployment:
|
| 94 |
+
|
| 95 |
+
```mermaid
|
| 96 |
+
flowchart TD
|
| 97 |
+
START["Server Startup"] --> CHECK["HF_API_TOKEN set?"]
|
| 98 |
+
CHECK -->|"Yes"| HF_MODE["HF API Mode"]
|
| 99 |
+
CHECK -->|"No"| LOCAL["Local Model Mode"]
|
| 100 |
+
|
| 101 |
+
HF_MODE --> LOAD_SUMM["Load Summarization<br/>(mBART, always local)"]
|
| 102 |
+
HF_MODE --> LAZY_SPELL["Lazy-load Spelling<br/>(on first request)"]
|
| 103 |
+
HF_MODE --> LAZY_GRAM["Lazy-load Grammar<br/>(on first request)"]
|
| 104 |
+
HF_MODE --> LAZY_PUNC["Lazy-load Punctuation<br/>(on first request)"]
|
| 105 |
+
|
| 106 |
+
LOCAL --> LOAD_ALL["Load All Models<br/>(Summarization, Spelling,<br/>Grammar, Punctuation)"]
|
| 107 |
+
|
| 108 |
+
LOAD_SUMM --> TRY_REMOTE["Try HF Hub Remote"]
|
| 109 |
+
TRY_REMOTE -->|"Success"| READY["Model Ready"]
|
| 110 |
+
TRY_REMOTE -->|"Fail"| TRY_LOCAL["Fallback to Local Path"]
|
| 111 |
+
TRY_LOCAL --> READY
|
| 112 |
+
```
|
| 113 |
+
|
| 114 |
+
**Key Design Decisions:**
|
| 115 |
+
|
| 116 |
+
1. **Lazy Loading**: Spelling, grammar, punctuation, autocomplete, and dialect models are loaded on first request (singleton pattern), not at server startup. This avoids blocking the health check endpoint and reduces cold-start time.
|
| 117 |
+
|
| 118 |
+
2. **CPU-Only Inference**: All models run on CPU (`torch.device('cpu')`) in production. The grammar model explicitly forces CPU even when CUDA is available, to avoid GPU OOM on shared infrastructure.
|
| 119 |
+
|
| 120 |
+
3. **Float16 Precision**: Summarization and dialect models use `torch.float16` to halve memory consumption. Grammar uses `torch.float32` for stability on CPU.
|
| 121 |
+
|
| 122 |
+
4. **Pre-Downloaded Models**: The Dockerfile pre-downloads all model weights during the Docker build phase, caching them in the HuggingFace Hub local cache. At runtime, the container has no outbound DNS, so models must be available locally.
|
| 123 |
+
|
| 124 |
+
### 3.3.3 The `/api/analyze` Pipeline
|
| 125 |
+
|
| 126 |
+
The `/api/analyze` endpoint is the most architecturally complex component of the backend. It orchestrates a three-stage sequential pipeline: Spelling → Grammar → Punctuation. Each stage's output feeds into the next stage's input, and a sophisticated coordinate mapping system tracks character offsets through text mutations to produce suggestions aligned with the user's original input.
|
| 127 |
+
|
| 128 |
+
```mermaid
|
| 129 |
+
sequenceDiagram
|
| 130 |
+
participant Client
|
| 131 |
+
participant Flask as Flask /api/analyze
|
| 132 |
+
participant Ctx as PipelineContext
|
| 133 |
+
participant Spell as AraSpell
|
| 134 |
+
participant Gram as Gemma 3 + CAMeL
|
| 135 |
+
participant Punc as PuncAra-v1
|
| 136 |
+
|
| 137 |
+
Client->>Flask: POST {text: "user input"}
|
| 138 |
+
Flask->>Ctx: PipelineContext(original_text)
|
| 139 |
+
Note over Ctx: original_text = IMMUTABLE
|
| 140 |
+
|
| 141 |
+
rect rgb(230, 245, 255)
|
| 142 |
+
Flask->>Spell: spell_checker.correct(current_text)
|
| 143 |
+
Spell-->>Flask: corrected text
|
| 144 |
+
Flask->>Flask: Word-level diff analysis
|
| 145 |
+
Flask->>Flask: Filter: _is_small_spelling_change()
|
| 146 |
+
Flask->>Ctx: ctx.add_patch('spelling', ...)
|
| 147 |
+
Flask->>Ctx: ctx.mutate_text(safe_text, OffsetMapper)
|
| 148 |
+
Note over Ctx: StageLocker locks spelling spans
|
| 149 |
+
end
|
| 150 |
+
|
| 151 |
+
rect rgb(255, 245, 230)
|
| 152 |
+
Flask->>Gram: grammar_checker.correct(current_text)
|
| 153 |
+
Gram-->>Flask: corrected text
|
| 154 |
+
Flask->>Flask: Word-level diff (get_word_diffs)
|
| 155 |
+
Flask->>Flask: StageLocker check (skip locked spans)
|
| 156 |
+
Flask->>Flask: Hallucination filter (Jaccard < 0.3)
|
| 157 |
+
Flask->>Flask: IV→OOV corruption guard
|
| 158 |
+
Flask->>Ctx: ctx.add_patch('grammar', ...)
|
| 159 |
+
Flask->>Ctx: ctx.mutate_text(corrected, OffsetMapper)
|
| 160 |
+
end
|
| 161 |
+
|
| 162 |
+
rect rgb(245, 255, 230)
|
| 163 |
+
Flask->>Punc: punc_checker.correct(current_text)
|
| 164 |
+
Punc-->>Flask: punctuated text
|
| 165 |
+
Flask->>Flask: Word-level diff
|
| 166 |
+
Flask->>Flask: StageLocker check (allow pure punct)
|
| 167 |
+
Flask->>Flask: validate_punctuation_diff()
|
| 168 |
+
Flask->>Ctx: ctx.add_patch('punctuation', ...)
|
| 169 |
+
Flask->>Flask: Cap at 3 punctuation patches
|
| 170 |
+
Flask->>Ctx: ctx.mutate_text(punctuated, OffsetMapper)
|
| 171 |
+
end
|
| 172 |
+
|
| 173 |
+
Flask->>Ctx: ctx.patches.to_list() (overlap resolution)
|
| 174 |
+
Flask->>Flask: _apply_patches_to_original()
|
| 175 |
+
Flask-->>Client: {original, corrected, suggestions[], timing_ms}
|
| 176 |
+
```
|
| 177 |
+
|
| 178 |
+
## 3.4 Pipeline Hardening Architecture
|
| 179 |
+
|
| 180 |
+
### 3.4.1 PipelineContext
|
| 181 |
+
|
| 182 |
+
The `PipelineContext` class (`src/nlp/pipeline_context.py`) carries all shared state through the three-stage pipeline. It enforces the following invariants:
|
| 183 |
+
|
| 184 |
+
1. **`original_text` is IMMUTABLE** — never reassigned after construction.
|
| 185 |
+
2. **`_offset_mappers` is APPEND-ONLY** — past mappers are never mutated or removed.
|
| 186 |
+
3. **`map_to_original()` is READ-ONLY** — deterministic coordinate transforms.
|
| 187 |
+
4. **All coordinate transforms go through `OffsetMapper` public API** — no direct access to internal opcodes.
|
| 188 |
+
|
| 189 |
+
### 3.4.2 CorrectionPatch and PatchSet
|
| 190 |
+
|
| 191 |
+
The `CorrectionPatch` dataclass (`src/nlp/correction_patch.py`) represents a single correction suggestion with dual coordinate spaces:
|
| 192 |
+
|
| 193 |
+
- **ORIGINAL coordinates** (`start_original`, `end_original`): Used for API response and overlap resolution. These coordinates refer to the user's original input text.
|
| 194 |
+
- **CURRENT coordinates** (`start_current`, `end_current`): Used by the `StageLocker` for pipeline-internal range checking. These coordinates refer to the pipeline's working copy, which is mutated by each stage.
|
| 195 |
+
|
| 196 |
+
The `PatchSet` class implements deterministic overlap resolution using a greedy first-fit strategy:
|
| 197 |
+
|
| 198 |
+
```
|
| 199 |
+
Sort order: priority DESC → confidence DESC → start ASC → id ASC
|
| 200 |
+
Strategy: First non-overlapping patch wins its range. One range = one owner.
|
| 201 |
+
```
|
| 202 |
+
|
| 203 |
+
**Priority hierarchy:**
|
| 204 |
+
|
| 205 |
+
| Stage | Priority |
|
| 206 |
+
|---|---|
|
| 207 |
+
| Grammar | 3 (highest) |
|
| 208 |
+
| Punctuation | 2 |
|
| 209 |
+
| Spelling | 1 |
|
| 210 |
+
| Autocomplete | 0 (lowest) |
|
| 211 |
+
|
| 212 |
+
### 3.4.3 OffsetMapper
|
| 213 |
+
|
| 214 |
+
The `OffsetMapper` class (`src/app.py`) provides bidirectional coordinate transformation between consecutive text versions using `difflib.SequenceMatcher`:
|
| 215 |
+
|
| 216 |
+
- **`reverse_map_offset(pos)`**: Maps a position from `text_after` → `text_before` (used to walk back to original coordinates).
|
| 217 |
+
- **`forward_map_range(start, end)`**: Maps a range from `text_before` → `text_after` (used by `StageLocker` to update locked spans after mutations).
|
| 218 |
+
- **Monotonicity guard**: If independent point mapping produces an inverted range (start > end), the end is clamped to `max(new_start, new_end)`.
|
| 219 |
+
|
| 220 |
+
### 3.4.4 StageLocker
|
| 221 |
+
|
| 222 |
+
The `StageLocker` (`src/nlp/stage_locker.py`) prevents later pipeline stages from modifying text ranges that were already corrected by earlier stages. When the spelling stage corrects a word, the `StageLocker` locks that character range. When the grammar stage subsequently proposes a correction overlapping with a locked range, the correction is rejected (unless it is a pure punctuation change).
|
| 223 |
+
|
| 224 |
+
```mermaid
|
| 225 |
+
flowchart LR
|
| 226 |
+
SPELL["Spelling corrects<br/>'هذة' → 'هذه'<br/>at [10:14]"]
|
| 227 |
+
LOCK["StageLocker.lock(10, 14, 'spelling')"]
|
| 228 |
+
GRAM["Grammar proposes<br/>change at [10:14]"]
|
| 229 |
+
CHECK["StageLocker.is_locked(10, 14)?"]
|
| 230 |
+
BLOCK["BLOCKED ✗"]
|
| 231 |
+
|
| 232 |
+
SPELL --> LOCK
|
| 233 |
+
GRAM --> CHECK
|
| 234 |
+
CHECK -->|"Yes"| BLOCK
|
| 235 |
+
```
|
| 236 |
+
|
| 237 |
+
## 3.5 NLP Model Architecture
|
| 238 |
+
|
| 239 |
+
### 3.5.1 AraSpell Spelling Correction Pipeline
|
| 240 |
+
|
| 241 |
+
```mermaid
|
| 242 |
+
flowchart TD
|
| 243 |
+
INPUT["Input Text"] --> PREPROCESS["Preprocessing<br/>• Remove diacritics<br/>• Remove tatweel<br/>• Normalize special chars<br/>• Collapse repeated chars<br/>• Fix char substitutions"]
|
| 244 |
+
PREPROCESS --> CLASSIFY["Error Classification<br/>• CHAR_REPETITION<br/>• WORD_MERGE<br/>• CHAR_SUBSTITUTION<br/>• MIXED<br/>• CLEAN"]
|
| 245 |
+
CLASSIFY --> RULES["Rules-Based Correction<br/>• Keyboard proximity<br/>• Recursive word splitting<br/>• Fragment joining"]
|
| 246 |
+
RULES --> MODEL["Neural Correction<br/>• AraBERT Encoder-Decoder<br/>• Beam search (num_beams=5)<br/>• max_length=128"]
|
| 247 |
+
MODEL --> VALIDATE["Output Validation<br/>• Length ratio check<br/>• Character preservation (Jaccard)<br/>• Word count check<br/>• Hallucination detection"]
|
| 248 |
+
VALIDATE --> ALIGN["Word Alignment<br/>• IV/OOV-based word selection<br/>• Hybrid word construction<br/>• ه→ة preference for IV-IV"]
|
| 249 |
+
ALIGN --> CONTEXT["Contextual Refinement<br/>• BERT MLM reranking<br/>• Top-k mask filling<br/>• Vocabulary validation"]
|
| 250 |
+
CONTEXT --> POST["Post-Processing<br/>• Remove hallucinations<br/>• Fix hamza (whitelist)<br/>• Fix ta marbuta<br/>• Merge fragments<br/>• Normalize spaces"]
|
| 251 |
+
POST --> OUTPUT["Corrected Text"]
|
| 252 |
+
```
|
| 253 |
+
|
| 254 |
+
**Architecture details of AraSpell:**
|
| 255 |
+
|
| 256 |
+
| Component | Class | Lines |
|
| 257 |
+
|---|---|---|
|
| 258 |
+
| Post-Processor | `AraSpellPostProcessor` | ~360 |
|
| 259 |
+
| Error Classifier | `ErrorClassifier` | ~40 |
|
| 260 |
+
| Rules-Based Corrector | `RulesBasedCorrector` | ~100 |
|
| 261 |
+
| Output Validator | `OutputValidator` | ~60 |
|
| 262 |
+
| Vocabulary Manager | `VocabularyManager` | ~80 |
|
| 263 |
+
| Word Aligner | `WordAligner` | ~65 |
|
| 264 |
+
| Split/Merge Specialist | `SplitMergeSpecialist` | ~100 |
|
| 265 |
+
| Contextual Corrector | `ContextualCorrector` | ~100 |
|
| 266 |
+
| Edit Distance Corrector | `EditDistanceCorrector` | ~150 |
|
| 267 |
+
| Main Spell Checker | `ArabicSpellChecker` | ~200 |
|
| 268 |
+
| **Total** | | **~1,507** |
|
| 269 |
+
|
| 270 |
+
### 3.5.2 Grammar Correction Architecture
|
| 271 |
+
|
| 272 |
+
```mermaid
|
| 273 |
+
flowchart LR
|
| 274 |
+
INPUT["Input Text"] --> GRADIO["Gradio Client<br/>(Gemma 3 Inference)"]
|
| 275 |
+
GRADIO --> CAMEL["ArabicGrammarGuard<br/>(CAMeL Tools)"]
|
| 276 |
+
CAMEL --> OUTPUT["Corrected Text"]
|
| 277 |
+
|
| 278 |
+
subgraph "ArabicGrammarGuard Rules"
|
| 279 |
+
R1["preserve_numbers()"]
|
| 280 |
+
R2["fix_number_and_gender_agreement()"]
|
| 281 |
+
R3["smart_asmaa_khamsa_fix()"]
|
| 282 |
+
R4["fix_verbs_nasb_and_jazm()"]
|
| 283 |
+
R5["fix_gender_agreement()"]
|
| 284 |
+
R6["fix_prepositions_advanced()"]
|
| 285 |
+
R7["fix_subject_verb_agreement()"]
|
| 286 |
+
R8["regex_rules_fallback()"]
|
| 287 |
+
end
|
| 288 |
+
|
| 289 |
+
CAMEL --> R1 --> R2 --> R3 --> R4 --> R5 --> R6 --> R7 --> R8
|
| 290 |
+
```
|
| 291 |
+
|
| 292 |
+
### 3.5.3 Punctuation Restoration Architecture
|
| 293 |
+
|
| 294 |
+
```mermaid
|
| 295 |
+
flowchart TD
|
| 296 |
+
INPUT["Input Text"] --> SPLIT["Split into Paragraphs"]
|
| 297 |
+
SPLIT --> CHUNK["Windowed Chunking<br/>(50 words/window,<br/>non-overlapping stride)"]
|
| 298 |
+
CHUNK --> PREPROC["arabic_preprocessing()<br/>• Remove diacritics<br/>• Normalize"]
|
| 299 |
+
PREPROC --> MODEL["PuncAra-v1 Inference<br/>• EncoderDecoderModel<br/>• num_beams=3<br/>• repetition_penalty=1.2"]
|
| 300 |
+
MODEL --> STRIP["Strip Non-Punct Changes<br/>(Fix P1: preserve only<br/>punctuation modifications)"]
|
| 301 |
+
STRIP --> POST["arabic_postprocessing()<br/>• Typographic cleanup<br/>• Space normalization"]
|
| 302 |
+
POST --> OUTPUT["Punctuated Text"]
|
| 303 |
+
```
|
| 304 |
+
|
| 305 |
+
### 3.5.4 Autocomplete Architecture
|
| 306 |
+
|
| 307 |
+
```mermaid
|
| 308 |
+
flowchart TD
|
| 309 |
+
INPUT["User Context<br/>(last ~200 chars)"] --> CHECK["GPT-2 Available?"]
|
| 310 |
+
CHECK -->|"Yes"| HYBRID["Hybrid Prediction"]
|
| 311 |
+
CHECK -->|"No"| BIGRAM["Bigram-Only Prediction"]
|
| 312 |
+
|
| 313 |
+
HYBRID --> STAT["Statistical (Bigram)<br/>• Last word → next word<br/>• Frequency-based ranking"]
|
| 314 |
+
HYBRID --> NEURAL["Neural (AraGPT2)<br/>• Full sentence context<br/>• Sampling (top_k=50, top_p=0.9)<br/>• 15 return sequences<br/>• Extract first Arabic word"]
|
| 315 |
+
|
| 316 |
+
STAT --> SCORE["Hybrid Scoring<br/>score = 0.4 × stat + 0.6 × neural"]
|
| 317 |
+
NEURAL --> SCORE
|
| 318 |
+
SCORE --> FILTER["Filter & Deduplicate<br/>• merge_similar_predictions()<br/>• Threshold ≥ 0.05<br/>• Return top-N"]
|
| 319 |
+
FILTER --> OUTPUT["Suggestions[]"]
|
| 320 |
+
|
| 321 |
+
BIGRAM --> LOOKUP["Bigram Lookup<br/>• Last word as key<br/>• Fallback to unigram"]
|
| 322 |
+
LOOKUP --> FILTER
|
| 323 |
+
```
|
| 324 |
+
|
| 325 |
+
## 3.6 Frontend Architecture (Web Application)
|
| 326 |
+
|
| 327 |
+
### 3.6.1 Single-Page Application Structure
|
| 328 |
+
|
| 329 |
+
The web application is a single HTML file (`src/index.html`, 147,459 bytes) with modular JavaScript:
|
| 330 |
+
|
| 331 |
+
```mermaid
|
| 332 |
+
graph TD
|
| 333 |
+
HTML["index.html<br/>(147KB)"]
|
| 334 |
+
HTML --> EDITOR["editor.js<br/>(30KB, WYSIWYG editor)"]
|
| 335 |
+
HTML --> RENDERER["renderer.js<br/>(12KB, results display)"]
|
| 336 |
+
HTML --> UI["ui.js<br/>(13KB, UI management)"]
|
| 337 |
+
HTML --> API["api.js<br/>(1.6KB, API client)"]
|
| 338 |
+
HTML --> FORMAT["format.js<br/>(12.7KB, formatting toolbar)"]
|
| 339 |
+
HTML --> SELECTION["selection.js<br/>(6.7KB, text selection)"]
|
| 340 |
+
HTML --> THEME["theme.js<br/>(2.4KB, theme management)"]
|
| 341 |
+
HTML --> AUTOCOMPLETE["autocomplete.js<br/>(15.5KB, autocomplete UI)"]
|
| 342 |
+
HTML --> AUTH["auth/ module<br/>(authentication)"]
|
| 343 |
+
HTML --> DOCS["documents/ module<br/>(local document management)"]
|
| 344 |
+
HTML --> DOCS_CLOUD["documents-cloud/ module<br/>(cloud sync via Supabase)"]
|
| 345 |
+
HTML --> SUMMARIES["summaries/ module<br/>(summary display)"]
|
| 346 |
+
```
|
| 347 |
+
|
| 348 |
+
### 3.6.2 Editor Architecture
|
| 349 |
+
|
| 350 |
+
The WYSIWYG editor is built on a `contenteditable` `<div>` element with custom JavaScript logic for:
|
| 351 |
+
|
| 352 |
+
- **Rich text formatting**: Bold, italic, underline, font family, font size, text alignment (right-to-left default for Arabic), text color, and highlight color.
|
| 353 |
+
- **Real-time analysis**: Debounced analysis requests sent to `/api/analyze` as the user types.
|
| 354 |
+
- **Autocomplete dropdown**: Context-aware suggestions triggered by text input, positioned near the cursor.
|
| 355 |
+
- **Inline highlighting**: Color-coded underlines for spelling (red), grammar (blue), and punctuation (green) suggestions.
|
| 356 |
+
- **Document management**: Create, save, load, rename, and delete documents with local storage persistence and optional Supabase cloud sync.
|
| 357 |
+
|
| 358 |
+
## 3.7 Chrome Extension Architecture
|
| 359 |
+
|
| 360 |
+
### 3.7.1 Manifest V3 Component Model
|
| 361 |
+
|
| 362 |
+
```mermaid
|
| 363 |
+
graph TD
|
| 364 |
+
subgraph "Browser Chrome"
|
| 365 |
+
POPUP["Popup<br/>(popup.html)"]
|
| 366 |
+
SIDE["Side Panel<br/>(sidepanel.html)"]
|
| 367 |
+
BG["Service Worker<br/>(background.js)"]
|
| 368 |
+
end
|
| 369 |
+
|
| 370 |
+
subgraph "Web Page"
|
| 371 |
+
CS["Content Script<br/>(content-inline.js)"]
|
| 372 |
+
OVERLAY["Highlight Overlay<br/>(CSS positioned spans)"]
|
| 373 |
+
FAB["Floating Action Button<br/>(bayan-fab)"]
|
| 374 |
+
TOOLTIP["Suggestion Tooltip<br/>(bayan-tooltip)"]
|
| 375 |
+
end
|
| 376 |
+
|
| 377 |
+
subgraph "Shared Modules"
|
| 378 |
+
CTRL["analysis-controller.js"]
|
| 379 |
+
BAPI["bayan-api.js"]
|
| 380 |
+
PATCHES["bayan-patches.js"]
|
| 381 |
+
RENDER["bayan-renderer.js"]
|
| 382 |
+
STATE["bayan-state.js"]
|
| 383 |
+
BAYAN_UI["bayan-ui.js"]
|
| 384 |
+
CONFIG["config.js"]
|
| 385 |
+
CONST["constants.js"]
|
| 386 |
+
HASH["hash.js"]
|
| 387 |
+
end
|
| 388 |
+
|
| 389 |
+
POPUP -->|"sendMessage"| BG
|
| 390 |
+
SIDE -->|"sendMessage"| BG
|
| 391 |
+
CS -->|"sendMessage"| BG
|
| 392 |
+
BG -->|"fetch()"| API["Backend API"]
|
| 393 |
+
|
| 394 |
+
POPUP -.-> CTRL
|
| 395 |
+
POPUP -.-> BAPI
|
| 396 |
+
POPUP -.-> RENDER
|
| 397 |
+
CS -.-> CONST
|
| 398 |
+
CS -.-> CTRL
|
| 399 |
+
```
|
| 400 |
+
|
| 401 |
+
### 3.7.2 Background Service Worker
|
| 402 |
+
|
| 403 |
+
The background service worker (`extension/background.js`, 6,213 bytes) serves three purposes:
|
| 404 |
+
|
| 405 |
+
1. **Network Proxy**: Content scripts cannot make cross-origin requests to the Bayan API. The service worker receives `BAYAN_ANALYZE` messages and proxies them via `fetch()`.
|
| 406 |
+
|
| 407 |
+
2. **Context Menu Registration**: Creates the right-click context menu item "✍️ تحليل مع بيان" that allows users to analyze selected text.
|
| 408 |
+
|
| 409 |
+
3. **Side Panel Management**: Responds to `OPEN_SIDE_PANEL` messages by calling `chrome.sidePanel.open()`.
|
| 410 |
+
|
| 411 |
+
### 3.7.3 Content Script — Inline Analysis Engine
|
| 412 |
+
|
| 413 |
+
The content-inline.js script (20,208 bytes) implements the Grammarly-style inline analysis:
|
| 414 |
+
|
| 415 |
+
```mermaid
|
| 416 |
+
statechart-v2
|
| 417 |
+
[*] --> Idle
|
| 418 |
+
Idle --> Detecting : User focuses editable field
|
| 419 |
+
Detecting --> Observing : Editable field detected
|
| 420 |
+
Observing --> Analyzing : Text changed (debounced 800ms)
|
| 421 |
+
Analyzing --> Rendering : API response received
|
| 422 |
+
Rendering --> Observing : Highlights rendered
|
| 423 |
+
Observing --> Idle : User blurs field
|
| 424 |
+
|
| 425 |
+
state Analyzing {
|
| 426 |
+
[*] --> SendMessage
|
| 427 |
+
SendMessage --> WaitResponse : chrome.runtime.sendMessage
|
| 428 |
+
WaitResponse --> ProcessPatches : Response.suggestions[]
|
| 429 |
+
ProcessPatches --> [*]
|
| 430 |
+
}
|
| 431 |
+
|
| 432 |
+
state Rendering {
|
| 433 |
+
[*] --> ClearOverlay
|
| 434 |
+
ClearOverlay --> CreateHighlights
|
| 435 |
+
CreateHighlights --> PositionOverlay
|
| 436 |
+
PositionOverlay --> [*]
|
| 437 |
+
}
|
| 438 |
+
```
|
| 439 |
+
|
| 440 |
+
**Key Design Features:**
|
| 441 |
+
|
| 442 |
+
- **MutationObserver**: Detects dynamically created editable fields on SPAs.
|
| 443 |
+
- **Debounced Analysis**: 800ms debounce prevents excessive API calls during rapid typing.
|
| 444 |
+
- **Content Hash Check**: Uses FNV-1a hashing (`shared/hash.js`) to skip re-analysis if text content hasn't changed.
|
| 445 |
+
- **Protected Site Detection**: Skips injection on `chrome://`, `chrome-extension://`, and Chrome Web Store domains.
|
| 446 |
+
- **Error Recovery Mode**: On API failure, enters a backoff state rather than repeatedly failing.
|
| 447 |
+
|
| 448 |
+
### 3.7.4 Shared Module Architecture
|
| 449 |
+
|
| 450 |
+
The shared modules implement a clean separation of concerns:
|
| 451 |
+
|
| 452 |
+
| Module | Responsibility |
|
| 453 |
+
|---|---|
|
| 454 |
+
| `constants.js` | API URL, version string |
|
| 455 |
+
| `config.js` | Configuration management |
|
| 456 |
+
| `hash.js` | FNV-1a content hashing |
|
| 457 |
+
| `bayan-api.js` | API client with error handling |
|
| 458 |
+
| `bayan-state.js` | Analysis state management |
|
| 459 |
+
| `bayan-patches.js` | Patch data model and operations |
|
| 460 |
+
| `bayan-renderer.js` | Result rendering (shared by popup/sidepanel) |
|
| 461 |
+
| `bayan-ui.js` | UI helper functions |
|
| 462 |
+
| `analysis-controller.js` | Orchestration: hash check → API call → state update → render |
|
| 463 |
+
|
| 464 |
+
## 3.8 Deployment Architecture
|
| 465 |
+
|
| 466 |
+
### 3.8.1 Docker Container
|
| 467 |
+
|
| 468 |
+
```mermaid
|
| 469 |
+
flowchart TD
|
| 470 |
+
subgraph "Docker Build Phase"
|
| 471 |
+
BASE["python:3.12-slim"]
|
| 472 |
+
DEPS["Install system deps<br/>(build-essential)"]
|
| 473 |
+
PIP["Install Python deps<br/>(CPU-only PyTorch)"]
|
| 474 |
+
DL_SUMM["Pre-download:<br/>Summarization (mBART)"]
|
| 475 |
+
DL_SPELL["Pre-download:<br/>Spelling (AraSpell + AraBERT)"]
|
| 476 |
+
DL_GRAM["Pre-download:<br/>Grammar (CAMeL data)"]
|
| 477 |
+
DL_PUNC["Pre-download:<br/>Punctuation (PuncAra-v1)"]
|
| 478 |
+
DL_DIAL["Pre-download:<br/>Dialect (mT5)"]
|
| 479 |
+
COPY["Copy src/, quran.py,<br/>quran_master.db, .env"]
|
| 480 |
+
end
|
| 481 |
+
|
| 482 |
+
subgraph "Runtime"
|
| 483 |
+
GUNICORN["gunicorn --chdir src app:app<br/>--bind 0.0.0.0:7860<br/>--timeout 300<br/>--workers 1"]
|
| 484 |
+
PORT["Expose 7860"]
|
| 485 |
+
end
|
| 486 |
+
|
| 487 |
+
BASE --> DEPS --> PIP
|
| 488 |
+
PIP --> DL_SUMM --> DL_SPELL --> DL_GRAM --> DL_PUNC --> DL_DIAL
|
| 489 |
+
DL_DIAL --> COPY --> GUNICORN --> PORT
|
| 490 |
+
```
|
| 491 |
+
|
| 492 |
+
**Key Parameters:**
|
| 493 |
+
- **Workers**: 1 (to minimize RAM on free tier)
|
| 494 |
+
- **Timeout**: 300s (to accommodate full pipeline: spelling ~50s + grammar ~8s + punctuation ~30s + cold start)
|
| 495 |
+
- **Port**: 7860 (HuggingFace Spaces default)
|
| 496 |
+
|
| 497 |
+
### 3.8.2 Authentication and Data Flow
|
| 498 |
+
|
| 499 |
+
```mermaid
|
| 500 |
+
sequenceDiagram
|
| 501 |
+
participant User
|
| 502 |
+
participant WebApp
|
| 503 |
+
participant Supabase
|
| 504 |
+
participant Flask as Bayan API
|
| 505 |
+
|
| 506 |
+
User->>WebApp: Login with email/password
|
| 507 |
+
WebApp->>Supabase: signInWithPassword()
|
| 508 |
+
Supabase-->>WebApp: JWT token + user profile
|
| 509 |
+
WebApp->>WebApp: Store session locally
|
| 510 |
+
|
| 511 |
+
User->>WebApp: Save document
|
| 512 |
+
WebApp->>Supabase: INSERT into documents table
|
| 513 |
+
Supabase-->>WebApp: Saved confirmation
|
| 514 |
+
|
| 515 |
+
User->>WebApp: Analyze text
|
| 516 |
+
WebApp->>Flask: POST /api/analyze {text}
|
| 517 |
+
Flask-->>WebApp: {original, corrected, suggestions[]}
|
| 518 |
+
WebApp->>WebApp: Render results
|
| 519 |
+
```
|
| 520 |
+
|
| 521 |
+
## 3.9 Data Models
|
| 522 |
+
|
| 523 |
+
### 3.9.1 Analysis API Response
|
| 524 |
+
|
| 525 |
+
```json
|
| 526 |
+
{
|
| 527 |
+
"original": "النص الأصلي",
|
| 528 |
+
"corrected": "النص المصحح",
|
| 529 |
+
"suggestions": [
|
| 530 |
+
{
|
| 531 |
+
"id": "uuid",
|
| 532 |
+
"start": 0,
|
| 533 |
+
"end": 5,
|
| 534 |
+
"original": "الأصلي",
|
| 535 |
+
"correction": "الأصلية",
|
| 536 |
+
"type": "spelling",
|
| 537 |
+
"priority": 1,
|
| 538 |
+
"confidence": 0.9,
|
| 539 |
+
"locked": true,
|
| 540 |
+
"alternatives": ["الأصلية", "الأصلي"]
|
| 541 |
+
}
|
| 542 |
+
],
|
| 543 |
+
"timing_ms": {
|
| 544 |
+
"spelling_ms": 1200,
|
| 545 |
+
"grammar_ms": 800,
|
| 546 |
+
"punctuation_ms": 500,
|
| 547 |
+
"total_ms": 2500
|
| 548 |
+
},
|
| 549 |
+
"status": "success"
|
| 550 |
+
}
|
| 551 |
+
```
|
| 552 |
+
|
| 553 |
+
### 3.9.2 CorrectionPatch Data Model
|
| 554 |
+
|
| 555 |
+
```mermaid
|
| 556 |
+
classDiagram
|
| 557 |
+
class CorrectionPatch {
|
| 558 |
+
+str stage
|
| 559 |
+
+int start_original
|
| 560 |
+
+int end_original
|
| 561 |
+
+int start_current
|
| 562 |
+
+int end_current
|
| 563 |
+
+str original
|
| 564 |
+
+str replacement
|
| 565 |
+
+int priority
|
| 566 |
+
+float confidence
|
| 567 |
+
+bool locked
|
| 568 |
+
+list alternatives
|
| 569 |
+
+str id
|
| 570 |
+
+dict to_dict()
|
| 571 |
+
}
|
| 572 |
+
|
| 573 |
+
class PatchSet {
|
| 574 |
+
+list patches
|
| 575 |
+
+add(patch)
|
| 576 |
+
+resolve_overlaps() list
|
| 577 |
+
+to_list() list
|
| 578 |
+
}
|
| 579 |
+
|
| 580 |
+
class PipelineContext {
|
| 581 |
+
+str original_text
|
| 582 |
+
+str current_text
|
| 583 |
+
+PatchSet patches
|
| 584 |
+
+list _offset_mappers
|
| 585 |
+
+StageLocker stage_locker
|
| 586 |
+
+map_to_original(start, end) tuple
|
| 587 |
+
+add_patch(stage, start, end, replacement, ...)
|
| 588 |
+
+mutate_text(text_after, OffsetMapperClass)
|
| 589 |
+
}
|
| 590 |
+
|
| 591 |
+
class StageLocker {
|
| 592 |
+
+list _locked_ranges
|
| 593 |
+
+lock(start, end, owner)
|
| 594 |
+
+is_locked(start, end) bool
|
| 595 |
+
+is_locked_by(start, end) tuple
|
| 596 |
+
+update_via_mapper(mapper)
|
| 597 |
+
}
|
| 598 |
+
|
| 599 |
+
class OffsetMapper {
|
| 600 |
+
-str _text_before
|
| 601 |
+
-str _text_after
|
| 602 |
+
-list _opcodes
|
| 603 |
+
+reverse_map_offset(pos) int
|
| 604 |
+
+forward_map_range(start, end) tuple
|
| 605 |
+
}
|
| 606 |
+
|
| 607 |
+
PipelineContext --> PatchSet
|
| 608 |
+
PipelineContext --> StageLocker
|
| 609 |
+
PipelineContext --> OffsetMapper
|
| 610 |
+
PatchSet --> CorrectionPatch
|
| 611 |
+
```
|
| 612 |
+
|
| 613 |
+
## 3.10 Security Considerations
|
| 614 |
+
|
| 615 |
+
### 3.10.1 Input Sanitization
|
| 616 |
+
|
| 617 |
+
The `/api/analyze` endpoint performs input sanitization:
|
| 618 |
+
|
| 619 |
+
1. **HTML Tag Stripping**: `re.sub(r'<[^>]*>', '', text)` removes HTML tags to prevent AraSpell from processing tag characters.
|
| 620 |
+
2. **Arabic Content Threshold**: Inputs with less than 30% Arabic characters (relative to total alphabetic characters) are returned without analysis, preventing code/markup from reaching the NLP models.
|
| 621 |
+
3. **Maximum Length**: All endpoints enforce a `MAX_TEXT_LENGTH = 5,000` character limit.
|
| 622 |
+
|
| 623 |
+
### 3.10.2 CORS Policy
|
| 624 |
+
|
| 625 |
+
```python
|
| 626 |
+
CORS(app, resources={r"/api/*": {"origins": "*"}})
|
| 627 |
+
```
|
| 628 |
+
|
| 629 |
+
CORS is restricted to `/api/*` routes only. Static file serving does not include CORS headers.
|
| 630 |
+
|
| 631 |
+
### 3.10.3 Chrome Extension Permissions
|
| 632 |
+
|
| 633 |
+
The manifest declares the minimum required permissions:
|
| 634 |
+
|
| 635 |
+
```json
|
| 636 |
+
"permissions": ["contextMenus", "activeTab", "storage", "sidePanel"],
|
| 637 |
+
"host_permissions": ["https://bayan10-bayan-api.hf.space/*"]
|
| 638 |
+
```
|
| 639 |
+
|
| 640 |
+
- No `<all_urls>` permission — only the Bayan API domain is allowed for network requests.
|
| 641 |
+
- `activeTab` provides temporary access to the current tab only when the user explicitly interacts with the extension.
|
| 642 |
+
- Content scripts are injected via the `content_scripts` manifest key (not programmatic injection), matching `https://*/*` and `http://*/*`.
|
| 643 |
+
|
| 644 |
+
## 3.11 Design Patterns Summary
|
| 645 |
+
|
| 646 |
+
| Pattern | Usage | Location |
|
| 647 |
+
|---|---|---|
|
| 648 |
+
| Singleton (Lazy-Loaded) | Model instances loaded on first request | All service modules |
|
| 649 |
+
| Pipeline | Sequential Spelling → Grammar → Punctuation processing | `/api/analyze` |
|
| 650 |
+
| Observer (MutationObserver) | Dynamic editable field detection | `content-inline.js` |
|
| 651 |
+
| Proxy | Service worker proxies API calls for content scripts | `background.js` |
|
| 652 |
+
| Strategy | Hybrid scoring selects between bigram and GPT-2 | `autocomplete_service.py` |
|
| 653 |
+
| Flyweight | Content hash avoids re-analysis of unchanged text | `analysis-controller.js` |
|
| 654 |
+
| Chain of Responsibility | OffsetMapper chain for coordinate transforms | `PipelineContext` |
|
| 655 |
+
| Greedy Algorithm | PatchSet overlap resolution | `correction_patch.py` |
|
docs/Chapter_4_Implementation.md
ADDED
|
@@ -0,0 +1,658 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Chapter 4: Implementation
|
| 2 |
+
|
| 3 |
+
## 4.1 Overview
|
| 4 |
+
|
| 5 |
+
This chapter details the implementation of each component of the Bayan system, covering the NLP pipeline, backend API, web application frontend, Chrome browser extension, and deployment infrastructure. For each component, we describe the implementation approach, key algorithms, data structures, and notable engineering decisions.
|
| 6 |
+
|
| 7 |
+
## 4.2 NLP Pipeline Implementation
|
| 8 |
+
|
| 9 |
+
### 4.2.1 AraSpell Spelling Correction
|
| 10 |
+
|
| 11 |
+
The AraSpell spelling correction system (`src/nlp/spelling/araspell_rules.py`, 1,507 lines) is the most complex NLP component in the system. It implements a multi-stage pipeline that processes text through nine distinct phases.
|
| 12 |
+
|
| 13 |
+
#### 4.2.1.1 Preprocessing (AraSpellPostProcessor)
|
| 14 |
+
|
| 15 |
+
The preprocessing stage normalizes input text to reduce noise before model inference:
|
| 16 |
+
|
| 17 |
+
```python
|
| 18 |
+
# Diacritics removal
|
| 19 |
+
text = re.sub(r'[ً-ْ]', '', text)
|
| 20 |
+
|
| 21 |
+
# Tatweel (kashida) removal
|
| 22 |
+
text = text.replace('ـ', '')
|
| 23 |
+
|
| 24 |
+
# Special character normalization (ligatures)
|
| 25 |
+
NORMALIZER_MAP = {'ﻹ': 'لإ', 'ﻷ': 'لأ', 'ﻵ': 'لآ', 'ﻻ': 'لا', 'ﷲ': 'الله'}
|
| 26 |
+
|
| 27 |
+
# Character repetition collapse (Arabic: 3+ → 1, Latin: 2+ → 1)
|
| 28 |
+
text = re.sub(r"([\u0600-\u06FF])\1{2,}", r"\1", text)
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
#### 4.2.1.2 Error Classification
|
| 32 |
+
|
| 33 |
+
The `ErrorClassifier` categorizes input text into one of five error types, which determines the correction strategy:
|
| 34 |
+
|
| 35 |
+
| Error Type | Detection Heuristic | Example |
|
| 36 |
+
|---|---|---|
|
| 37 |
+
| `CHAR_REPETITION` | 3+ consecutive identical Arabic characters | "كتاااااب" |
|
| 38 |
+
| `WORD_MERGE` | Words > 8 characters or single word > 6 chars | "فيالمدرسة" |
|
| 39 |
+
| `CHAR_SUBSTITUTION` | Non-Arabic keyboard characters (پ, گ, چ, etc.) | "پيت" (Persian ب) |
|
| 40 |
+
| `MIXED` | 2+ of the above | — |
|
| 41 |
+
| `CLEAN` | None of the above | "كتاب" |
|
| 42 |
+
|
| 43 |
+
#### 4.2.1.3 Rules-Based Correction
|
| 44 |
+
|
| 45 |
+
The `RulesBasedCorrector` applies deterministic corrections before model inference:
|
| 46 |
+
|
| 47 |
+
1. **Character Substitution Map**: Maps non-Arabic keyboard characters to their Arabic equivalents (40 mappings, e.g., 'ک' → 'ك', 'ی' → 'ي').
|
| 48 |
+
|
| 49 |
+
2. **Keyboard Proximity Map**: Maps 47 Arabic keyboard keys to their physical neighbors for error detection (e.g., 'ض' → ['ص', 'ق']).
|
| 50 |
+
|
| 51 |
+
3. **Recursive Word Splitting**: Detects merged prepositions using a longest-prefix-first algorithm:
|
| 52 |
+
```python
|
| 53 |
+
separables = sorted(['من', 'في', 'على', ...], key=len, reverse=True)
|
| 54 |
+
for sep in separables:
|
| 55 |
+
if word.startswith(sep) and len(remainder) >= 3:
|
| 56 |
+
return sep + " " + recursive_split(remainder)
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
#### 4.2.1.4 Neural Correction (AraBERT Encoder-Decoder)
|
| 60 |
+
|
| 61 |
+
The neural correction stage uses an AraBERT-based Encoder-Decoder model:
|
| 62 |
+
|
| 63 |
+
- **Encoder**: AraBERT (aubmindlab/bert-base-arabertv02), 12 layers, 768 hidden size, 12 attention heads, 64,000 vocabulary
|
| 64 |
+
- **Decoder**: AraBERT with cross-attention (is_decoder=True, add_cross_attention=True)
|
| 65 |
+
- **Training**: Fine-tuned on pairs of (misspelled, corrected) Arabic text
|
| 66 |
+
- **Inference**: Beam search with num_beams=5, max_length=128
|
| 67 |
+
|
| 68 |
+
Model weights are stored as a PyTorch checkpoint (`last_model.pt`) on HuggingFace Hub (`bayan10/AraSpell-Model`). At runtime, the model is assembled by:
|
| 69 |
+
|
| 70 |
+
```python
|
| 71 |
+
# Build architecture from config
|
| 72 |
+
config_encoder = BertConfig(vocab_size=64000, hidden_size=768, ...)
|
| 73 |
+
config_decoder = BertConfig(vocab_size=64000, ..., is_decoder=True, add_cross_attention=True)
|
| 74 |
+
model = EncoderDecoderModel(config=EncoderDecoderConfig.from_encoder_decoder_configs(...))
|
| 75 |
+
|
| 76 |
+
# Load trained weights
|
| 77 |
+
checkpoint = torch.load(model_path, map_location=device)
|
| 78 |
+
model.load_state_dict(checkpoint['model_state_dict'])
|
| 79 |
+
```
|
| 80 |
+
|
| 81 |
+
#### 4.2.1.5 Output Validation
|
| 82 |
+
|
| 83 |
+
The `OutputValidator` prevents hallucinated corrections from reaching the user:
|
| 84 |
+
|
| 85 |
+
```python
|
| 86 |
+
def validate(self, original, corrected, error_type):
|
| 87 |
+
# Length check: corrected must be 0.5x–2.5x original length
|
| 88 |
+
if len(corrected) > len(original) * 2.5: return False, "too_long"
|
| 89 |
+
if len(corrected) < len(original) * 0.5: return False, "too_short"
|
| 90 |
+
|
| 91 |
+
# Word count check: ratio must be 0.5–2.0
|
| 92 |
+
ratio = len(corrected.split()) / max(1, len(original.split()))
|
| 93 |
+
if ratio > 2.0 or ratio < 0.5: return False, "word_count_mismatch"
|
| 94 |
+
|
| 95 |
+
# Character preservation: Jaccard similarity must be ≥ 0.35
|
| 96 |
+
jaccard = len(chars_orig & chars_corr) / len(chars_orig | chars_corr)
|
| 97 |
+
if jaccard < 0.35: return False, "low_character_similarity"
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
#### 4.2.1.6 Word Alignment (Hybrid Correction)
|
| 101 |
+
|
| 102 |
+
The `WordAligner` selects the best word from {input_word, output_word, hybrid} for each position:
|
| 103 |
+
|
| 104 |
+
```
|
| 105 |
+
if input_word == output_word → keep
|
| 106 |
+
if input is OOV, output is IV → use output (correct misspelling)
|
| 107 |
+
if input is IV, output is OOV → keep input (prevent corruption)
|
| 108 |
+
if both IV → keep input (prevent meaning change)
|
| 109 |
+
EXCEPTION: if only difference is ه→ة at word end → use output (correct orthography)
|
| 110 |
+
```
|
| 111 |
+
|
| 112 |
+
#### 4.2.1.7 Contextual Refinement (BERT MLM)
|
| 113 |
+
|
| 114 |
+
For ambiguous corrections, the `ContextualCorrector` uses BERT's Masked Language Model to score candidate words in context:
|
| 115 |
+
|
| 116 |
+
```python
|
| 117 |
+
# Mask the target word and get top-k predictions
|
| 118 |
+
masked_text = text.replace(word, '[MASK]')
|
| 119 |
+
outputs = mlm_model(**tokenizer(masked_text, return_tensors='pt'))
|
| 120 |
+
top_k_tokens = torch.topk(outputs.logits[0, mask_pos], k=10)
|
| 121 |
+
|
| 122 |
+
# Select the candidate with highest contextual probability
|
| 123 |
+
for candidate in [original, correction]:
|
| 124 |
+
if candidate in top_k_tokens:
|
| 125 |
+
score = softmax_prob[candidate]
|
| 126 |
+
```
|
| 127 |
+
|
| 128 |
+
#### 4.2.1.8 Hamza Whitelist
|
| 129 |
+
|
| 130 |
+
The system maintains a curated whitelist of 60+ common hamza placement errors (`AraSpellPostProcessor.HAMZA_WHITELIST`):
|
| 131 |
+
|
| 132 |
+
```python
|
| 133 |
+
HAMZA_WHITELIST = {
|
| 134 |
+
'الي': 'إلى', 'الى': 'إلى',
|
| 135 |
+
'انت': 'أنت', 'انتم': 'أنتم',
|
| 136 |
+
'لان': 'لأن', 'لانه': 'لأنه',
|
| 137 |
+
'اذا': 'إذا', 'ايضا': 'أيضاً',
|
| 138 |
+
# ... 50+ more entries
|
| 139 |
+
}
|
| 140 |
+
```
|
| 141 |
+
|
| 142 |
+
The whitelist also handles prefixed forms using `HAMZA_PREFIXES`:
|
| 143 |
+
```python
|
| 144 |
+
HAMZA_PREFIXES = ['وبال', 'فبال', 'وال', 'بال', 'فال', 'كال',
|
| 145 |
+
'ول', 'فل', 'وب', 'فب', 'وك', 'فك', 'و', 'ف', 'ب', 'ك', 'ل']
|
| 146 |
+
```
|
| 147 |
+
|
| 148 |
+
This allows corrections like "واصدقائي" → "وأصدقائي" (prefix "و" + whitelist word "اصدقائي" → "أصدقائي").
|
| 149 |
+
|
| 150 |
+
### 4.2.2 Grammar Correction
|
| 151 |
+
|
| 152 |
+
#### 4.2.2.1 Model Inference (Gradio Client)
|
| 153 |
+
|
| 154 |
+
The grammar model uses a fine-tuned Gemma 3 model hosted on Gradio Spaces:
|
| 155 |
+
|
| 156 |
+
```python
|
| 157 |
+
from gradio_client import Client
|
| 158 |
+
client = Client("mohammedahmedezz2004/bayan_arabic_grammarly_correction")
|
| 159 |
+
model_output = client.predict(text=text, api_name="/correct_grammar")
|
| 160 |
+
```
|
| 161 |
+
|
| 162 |
+
The Gradio connection includes retry logic with exponential backoff for rate limiting and sleeping Spaces:
|
| 163 |
+
|
| 164 |
+
```python
|
| 165 |
+
for attempt in range(1, max_retries + 1):
|
| 166 |
+
try:
|
| 167 |
+
client = Client(GRADIO_SPACE)
|
| 168 |
+
break
|
| 169 |
+
except Exception as conn_err:
|
| 170 |
+
if is_retryable and attempt < max_retries:
|
| 171 |
+
time.sleep(2 ** attempt) # 2s, 4s, 8s
|
| 172 |
+
```
|
| 173 |
+
|
| 174 |
+
**Transient error handling**: Rate limiting, timeout, and connection errors are NOT cached — the next request retries loading. Only permanent failures are cached.
|
| 175 |
+
|
| 176 |
+
#### 4.2.2.2 Rule-Based Post-Processing (ArabicGrammarGuard)
|
| 177 |
+
|
| 178 |
+
The `ArabicGrammarGuard` class applies 8 rule categories using CAMeL Tools MLE Disambiguator:
|
| 179 |
+
|
| 180 |
+
**Rule 1: Number Preservation**
|
| 181 |
+
```python
|
| 182 |
+
def preserve_numbers(self, original_text, generated_text):
|
| 183 |
+
orig_digits = re.findall(r'\d+', original_text)
|
| 184 |
+
gen_digits = re.findall(r'\d+', generated_text)
|
| 185 |
+
if orig_digits and gen_digits and orig_digits != gen_digits:
|
| 186 |
+
return original_text # Reject if digits changed
|
| 187 |
+
```
|
| 188 |
+
|
| 189 |
+
**Rule 2: Number-Gender Agreement**
|
| 190 |
+
Uses morphological analysis to detect agreement errors between nouns and their modifying verbs/adjectives.
|
| 191 |
+
|
| 192 |
+
**Rule 3: Five Nouns (الأسماء الخمسة)**
|
| 193 |
+
```python
|
| 194 |
+
asmaa_khamsa_roots = ['اب', 'اخ', 'حم', 'فو', 'ذو']
|
| 195 |
+
# After إنّ and sisters: nominative → accusative (و→ا)
|
| 196 |
+
# After prepositions: nominative → genitive (و→ي)
|
| 197 |
+
```
|
| 198 |
+
|
| 199 |
+
**Rule 4: Verb Nasb and Jazm**
|
| 200 |
+
```python
|
| 201 |
+
nasb_particles = ['أن', 'لن', 'كي', 'لكي', 'حتى', 'إذن']
|
| 202 |
+
jazm_particles = ['لم', 'لما', 'لا']
|
| 203 |
+
# After nasb/jazm: remove ون ending → وا, ان → ا, ين → ي
|
| 204 |
+
```
|
| 205 |
+
|
| 206 |
+
**Rule 5: Gender Agreement for Demonstratives and Numbers**
|
| 207 |
+
```python
|
| 208 |
+
# هذان + feminine dual → هاتان
|
| 209 |
+
text = re.sub(r'\bهذان\s+(ال[أ-ي]+تان)\b', r'هاتان \1', text)
|
| 210 |
+
```
|
| 211 |
+
|
| 212 |
+
**Rule 6: Preposition Case Marking**
|
| 213 |
+
```python
|
| 214 |
+
# في المهندسون → في المهندسين (nominative → genitive after preposition)
|
| 215 |
+
# Requires stem ≥ 4 chars to avoid false positives on roots ending in ان
|
| 216 |
+
text = re.sub(r'\b([وف]?(?:في|من|إلى|على|عن|حتى))\s+([أ-ي]{4,})(ون|ان)\b', r'\1 \2ين', text)
|
| 217 |
+
```
|
| 218 |
+
|
| 219 |
+
**Rule 7: Subject-Verb Agreement (SVO Order)**
|
| 220 |
+
Detects confirmed plural nouns followed by singular verbs in SVO word order:
|
| 221 |
+
|
| 222 |
+
```python
|
| 223 |
+
KNOWN_PLURALS_MASC = {'الطلاب', 'طلاب', 'الرجال', 'رجال', ...}
|
| 224 |
+
KNOWN_PLURALS_FEM = {'الطالبات', 'طالبات', 'النساء', 'نساء', ...}
|
| 225 |
+
|
| 226 |
+
# Also detects sound masculine plurals (ending in ون/ين, ≥5 chars)
|
| 227 |
+
# and sound feminine plurals (ending in ات, ≥5 chars)
|
| 228 |
+
```
|
| 229 |
+
|
| 230 |
+
### 4.2.3 Punctuation Restoration (PuncAra-v1)
|
| 231 |
+
|
| 232 |
+
#### 4.2.3.1 Model Architecture
|
| 233 |
+
|
| 234 |
+
PuncAra-v1 is an EncoderDecoderModel built from AraBERT, fine-tuned for punctuation restoration:
|
| 235 |
+
|
| 236 |
+
```python
|
| 237 |
+
model = EncoderDecoderModel.from_pretrained("bayan10/PuncAra-v1")
|
| 238 |
+
model.config.decoder_start_token_id = tokenizer.cls_token_id
|
| 239 |
+
model.config.eos_token_id = tokenizer.sep_token_id
|
| 240 |
+
```
|
| 241 |
+
|
| 242 |
+
**Inference parameters:**
|
| 243 |
+
- `max_length=128` per chunk
|
| 244 |
+
- `num_beams=3` (beam search for punctuation accuracy)
|
| 245 |
+
- `repetition_penalty=1.2`
|
| 246 |
+
- `early_stopping=True`
|
| 247 |
+
|
| 248 |
+
#### 4.2.3.2 Windowed Chunking
|
| 249 |
+
|
| 250 |
+
Long texts are processed in 50-word non-overlapping windows:
|
| 251 |
+
|
| 252 |
+
```python
|
| 253 |
+
window_size = 50
|
| 254 |
+
stride = 50 # Non-overlapping
|
| 255 |
+
for i in range(0, total_words, stride):
|
| 256 |
+
chunk = words[i:i+window_size]
|
| 257 |
+
processed = predict_chunk(" ".join(chunk))
|
| 258 |
+
# Remove trailing punctuation from non-last segments
|
| 259 |
+
if not is_last and processed[-1] in ".?!،؛:؟!":
|
| 260 |
+
processed = processed[:-1]
|
| 261 |
+
```
|
| 262 |
+
|
| 263 |
+
#### 4.2.3.3 Non-Punctuation Change Stripping (Fix P1)
|
| 264 |
+
|
| 265 |
+
The PuncAra model was trained on data containing spelling/grammar corrections alongside punctuation. The `_strip_non_punctuation_changes()` method ensures only punctuation modifications are retained:
|
| 266 |
+
|
| 267 |
+
```python
|
| 268 |
+
for o_word, p_word in aligned_pairs:
|
| 269 |
+
o_base = strip_punct(o_word)
|
| 270 |
+
p_base = strip_punct(p_word)
|
| 271 |
+
if o_base == p_base:
|
| 272 |
+
result.append(p_word) # Same base — keep model's punctuation
|
| 273 |
+
else:
|
| 274 |
+
# Model changed word content — revert to original, keep new punctuation
|
| 275 |
+
result.append(o_word + new_punctuation_suffix)
|
| 276 |
+
```
|
| 277 |
+
|
| 278 |
+
### 4.2.4 Text Summarization (mBART)
|
| 279 |
+
|
| 280 |
+
The summarization model (`SummarizationModel` in `model_loader.py`) uses a fine-tuned mBART:
|
| 281 |
+
|
| 282 |
+
```python
|
| 283 |
+
generate_kwargs = dict(
|
| 284 |
+
max_new_tokens=max(20, min(max_length, 160)),
|
| 285 |
+
min_new_tokens=max(0, min_length),
|
| 286 |
+
num_beams=1, # Greedy decoding — empirically best for Arabic
|
| 287 |
+
do_sample=False,
|
| 288 |
+
early_stopping=False,
|
| 289 |
+
no_repeat_ngram_size=3,
|
| 290 |
+
repetition_penalty=1.1,
|
| 291 |
+
)
|
| 292 |
+
```
|
| 293 |
+
|
| 294 |
+
**Hallucination Detection and Extractive Fallback:**
|
| 295 |
+
|
| 296 |
+
```python
|
| 297 |
+
def _needs_fallback(self, source_text, summary_text):
|
| 298 |
+
source_words = set(source_text.split())
|
| 299 |
+
summary_words = summary_text.split()
|
| 300 |
+
overlap = sum(1 for w in summary_words if w in source_words)
|
| 301 |
+
overlap_ratio = overlap / max(1, len(summary_words))
|
| 302 |
+
ratio = SequenceMatcher(None, source[:500], summary[:500]).ratio()
|
| 303 |
+
return overlap_ratio < 0.35 or ratio < 0.22
|
| 304 |
+
```
|
| 305 |
+
|
| 306 |
+
When the model produces hallucinated output, the system falls back to an extractive approach that selects the opening sentences of the source text.
|
| 307 |
+
|
| 308 |
+
### 4.2.5 Dialect-to-MSA Conversion (mT5)
|
| 309 |
+
|
| 310 |
+
The dialect converter uses a fine-tuned mT5 model (`bayan10/dialect-to-msa-model`):
|
| 311 |
+
|
| 312 |
+
```python
|
| 313 |
+
class DialectConverter:
|
| 314 |
+
PREFIX = "حوّل إلى الفصحى: " # "Convert to MSA: "
|
| 315 |
+
|
| 316 |
+
def convert(self, dialect_text, num_beams=4):
|
| 317 |
+
input_text = self.PREFIX + dialect_text.strip()
|
| 318 |
+
outputs = self.model.generate(
|
| 319 |
+
**inputs,
|
| 320 |
+
max_length=128,
|
| 321 |
+
num_beams=num_beams,
|
| 322 |
+
early_stopping=True,
|
| 323 |
+
no_repeat_ngram_size=3,
|
| 324 |
+
)
|
| 325 |
+
```
|
| 326 |
+
|
| 327 |
+
The task-specific prefix "حوّل إلى الفصحى: " instructs the model to perform dialect-to-MSA conversion, following the T5 text-to-text format.
|
| 328 |
+
|
| 329 |
+
### 4.2.6 Autocomplete (Hybrid Bigram + AraGPT2)
|
| 330 |
+
|
| 331 |
+
#### 4.2.6.1 Bigram Model
|
| 332 |
+
|
| 333 |
+
The statistical component uses a pre-trained bigram model (`bayan10/AutoComplete/bigram_model_v4.pkl`) stored as a pickle file containing:
|
| 334 |
+
- `unigrams`: `Dict[str, int]` — word → frequency count
|
| 335 |
+
- `bigrams`: `Dict[str, Dict[str, int]]` — context_word → {next_word → count}
|
| 336 |
+
|
| 337 |
+
#### 4.2.6.2 GPT-2 Component
|
| 338 |
+
|
| 339 |
+
The neural component uses AraGPT2-Base (`aubmindlab/aragpt2-base`) with a sampling-based prediction strategy:
|
| 340 |
+
|
| 341 |
+
```python
|
| 342 |
+
outputs = self.gpt2_model.generate(
|
| 343 |
+
**inputs,
|
| 344 |
+
max_new_tokens=5,
|
| 345 |
+
do_sample=True, # Sampling for diversity (beam search collapses)
|
| 346 |
+
top_k=50,
|
| 347 |
+
top_p=0.9,
|
| 348 |
+
temperature=0.8,
|
| 349 |
+
num_return_sequences=15, # Generate 15 diverse sequences
|
| 350 |
+
)
|
| 351 |
+
|
| 352 |
+
# Extract first Arabic word from each sequence
|
| 353 |
+
for seq in outputs:
|
| 354 |
+
match = re.search(r'[\u0600-\u06FF]{2,}', generated_text)
|
| 355 |
+
word_counts[match.group(0)] += 1
|
| 356 |
+
|
| 357 |
+
# Score = frequency across samples
|
| 358 |
+
prob_dict = {w: count / total for w, count in word_counts.items()}
|
| 359 |
+
```
|
| 360 |
+
|
| 361 |
+
#### 4.2.6.3 Hybrid Scoring
|
| 362 |
+
|
| 363 |
+
```python
|
| 364 |
+
score = alpha * stat_prob + (1 - alpha) * neural_prob
|
| 365 |
+
# alpha = 0.4 → 40% bigram, 60% GPT-2
|
| 366 |
+
# threshold = 0.05 → minimum score to show
|
| 367 |
+
```
|
| 368 |
+
|
| 369 |
+
### 4.2.7 Quranic Text Verification
|
| 370 |
+
|
| 371 |
+
The Quran search engine (`quran.py`, 15,974 bytes) provides fuzzy search against a comprehensive SQLite database (`quran_master.db`, ~22MB). The database contains the complete Quran with:
|
| 372 |
+
- Arabic text (original and simplified)
|
| 373 |
+
- Verse metadata (Surah number, Ayah number, Surah name)
|
| 374 |
+
- Translations in multiple languages
|
| 375 |
+
|
| 376 |
+
The search function `search_bayan(text, target_type)` supports:
|
| 377 |
+
- **Verse verification**: Identifies if a given text is a Quranic quotation
|
| 378 |
+
- **Fuzzy matching**: Handles partial or slightly misspelled quotations
|
| 379 |
+
- **Translation lookup**: Returns translations alongside the Arabic text
|
| 380 |
+
|
| 381 |
+
## 4.3 Backend API Implementation
|
| 382 |
+
|
| 383 |
+
### 4.3.1 The `/api/analyze` Pipeline — Filtering and Safety
|
| 384 |
+
|
| 385 |
+
The `/api/analyze` endpoint implements extensive filtering at each pipeline stage to prevent over-correction and hallucination.
|
| 386 |
+
|
| 387 |
+
#### Spelling Stage Filtering
|
| 388 |
+
|
| 389 |
+
The `_is_small_spelling_change()` function implements a multi-layered filtering strategy:
|
| 390 |
+
|
| 391 |
+
1. **Numeral Protection**: Rejects any correction involving digits (Arabic or Latin).
|
| 392 |
+
2. **Directional Blocks**: Prevents known meaning-changing substitutions:
|
| 393 |
+
```python
|
| 394 |
+
_DIRECTIONAL_BLOCKS = {
|
| 395 |
+
'هذه': {'هذة'}, # Correct feminine → misspelling
|
| 396 |
+
'كان': {'كأن'}, # "was" → "as if" (meaning change)
|
| 397 |
+
'إلى': {'على', 'علي'}, # Different prepositions
|
| 398 |
+
}
|
| 399 |
+
```
|
| 400 |
+
3. **Clitic-Aware Blocking**: Applies directional blocks with common prefixes stripped (و+كان→و+كأن).
|
| 401 |
+
4. **Feminine Marker Protection**: Rejects corrections that drop ه/ة endings.
|
| 402 |
+
5. **In-Vocabulary Guard**: When both words are valid Arabic words, only accepts:
|
| 403 |
+
- Known ه→ة orthographic fixes (with pronoun suffix guard for ته patterns)
|
| 404 |
+
- Hamza whitelist matches (exact target match required)
|
| 405 |
+
6. **Levenshtein Filter**: For OOV words, rejects edits with distance > 2 or ratio > 50%.
|
| 406 |
+
7. **Orthographic-Only Filter**: Only allows character changes within the orthographic pairs set (ه↔ة, ا↔أ↔إ↔آ, ي↔ى, ؤ↔و, ئ↔ي, ء↔أ).
|
| 407 |
+
8. **Confidence Dampening**: Returns 0.5 (instead of 0.9) for OOV→IV corrections and hamza-only changes.
|
| 408 |
+
|
| 409 |
+
#### Grammar Stage Filtering
|
| 410 |
+
|
| 411 |
+
1. **StageLocker Check**: Skips diffs overlapping with spelling-locked ranges.
|
| 412 |
+
2. **Hallucination Rejection**: Rejects corrections with Jaccard character similarity < 0.3.
|
| 413 |
+
3. **IV→OOV Corruption Guard**: Rejects corrections that change a valid word to a non-word.
|
| 414 |
+
4. **Spelling Re-labeling**: If a grammar correction is purely orthographic (ه→ة, hamza), re-labels it as 'spelling' for correct UI icons.
|
| 415 |
+
5. **Bracket Balance Guard**: Rejects grammar output if it breaks bracket balance.
|
| 416 |
+
|
| 417 |
+
#### Punctuation Stage Filtering
|
| 418 |
+
|
| 419 |
+
1. **StageLocker Check**: Blocks word changes in locked ranges but allows pure punctuation insertions.
|
| 420 |
+
2. **`validate_punctuation_diff()`**: Validates that each diff only adds/modifies punctuation characters.
|
| 421 |
+
3. **Aggregate Cap**: Maximum 3 punctuation patches per response.
|
| 422 |
+
|
| 423 |
+
### 4.3.2 Smart Text Processing Strategy
|
| 424 |
+
|
| 425 |
+
The pipeline adapts its behavior based on text length:
|
| 426 |
+
|
| 427 |
+
| Text Length | Strategy | Reason |
|
| 428 |
+
|---|---|---|
|
| 429 |
+
| 0–300 chars | Full pipeline (Spelling + Grammar + Punctuation) | Complete analysis |
|
| 430 |
+
| 300–1000 chars | Grammar + Punctuation only | AraSpell too slow |
|
| 431 |
+
| 1000+ chars | Grammar + Punctuation only | Performance |
|
| 432 |
+
|
| 433 |
+
### 4.3.3 Supabase Integration
|
| 434 |
+
|
| 435 |
+
The backend injects Supabase credentials into the frontend at serving time:
|
| 436 |
+
|
| 437 |
+
```python
|
| 438 |
+
@app.route('/')
|
| 439 |
+
def index():
|
| 440 |
+
html = html_path.read_text(encoding='utf-8')
|
| 441 |
+
html = html.replace('<meta name="supabase-url" content="">',
|
| 442 |
+
f'<meta name="supabase-url" content="{SUPABASE_URL}">')
|
| 443 |
+
html = html.replace('<meta name="supabase-anon-key" content="">',
|
| 444 |
+
f'<meta name="supabase-anon-key" content="{SUPABASE_ANON_KEY}">')
|
| 445 |
+
```
|
| 446 |
+
|
| 447 |
+
## 4.4 Web Application Frontend
|
| 448 |
+
|
| 449 |
+
### 4.4.1 Editor Implementation
|
| 450 |
+
|
| 451 |
+
The WYSIWYG editor (`src/js/editor.js`, 30,022 bytes) provides:
|
| 452 |
+
|
| 453 |
+
- **ContentEditable-based editing**: Uses a `<div contenteditable="true">` element with custom event handlers for keyboard input, paste, and formatting commands.
|
| 454 |
+
- **RTL Default**: All text is right-to-left by default, appropriate for Arabic content.
|
| 455 |
+
- **Formatting Toolbar**: Bold (`document.execCommand('bold')`), italic, underline, font family, font size, text alignment, text color, and highlight color.
|
| 456 |
+
- **Real-time Analysis**: Debounced calls to `/api/analyze` on text change.
|
| 457 |
+
- **Inline Highlights**: Color-coded underlines beneath detected errors.
|
| 458 |
+
|
| 459 |
+
### 4.4.2 Theme System
|
| 460 |
+
|
| 461 |
+
The theme system (`src/js/theme.js`, 2,406 bytes) provides light and dark modes using CSS custom properties:
|
| 462 |
+
|
| 463 |
+
```css
|
| 464 |
+
:root {
|
| 465 |
+
--bg-primary: #ffffff;
|
| 466 |
+
--text-primary: #1a1a2e;
|
| 467 |
+
--accent: #667eea;
|
| 468 |
+
}
|
| 469 |
+
[data-theme="dark"] {
|
| 470 |
+
--bg-primary: #1a1a2e;
|
| 471 |
+
--text-primary: #e0e0e0;
|
| 472 |
+
--accent: #7c8cf8;
|
| 473 |
+
}
|
| 474 |
+
```
|
| 475 |
+
|
| 476 |
+
### 4.4.3 Document Management
|
| 477 |
+
|
| 478 |
+
Documents are stored in localStorage with the following operations:
|
| 479 |
+
- **Create**: Generates a new document with a unique ID and default title.
|
| 480 |
+
- **Save**: Serializes editor content to JSON and persists to localStorage.
|
| 481 |
+
- **Load**: Restores document content and metadata from localStorage.
|
| 482 |
+
- **Cloud Sync**: Optionally syncs documents to Supabase for cross-device access.
|
| 483 |
+
|
| 484 |
+
## 4.5 Chrome Extension Implementation
|
| 485 |
+
|
| 486 |
+
### 4.5.1 Background Service Worker
|
| 487 |
+
|
| 488 |
+
The background script (`extension/background.js`, 6,213 bytes) handles:
|
| 489 |
+
|
| 490 |
+
```javascript
|
| 491 |
+
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
| 492 |
+
if (msg.type === 'BAYAN_ANALYZE') {
|
| 493 |
+
fetch(API_URL + '/api/analyze', {
|
| 494 |
+
method: 'POST',
|
| 495 |
+
headers: {'Content-Type': 'application/json'},
|
| 496 |
+
body: JSON.stringify({text: msg.text})
|
| 497 |
+
})
|
| 498 |
+
.then(r => r.json())
|
| 499 |
+
.then(data => sendResponse(data))
|
| 500 |
+
.catch(err => sendResponse({error: err.message}));
|
| 501 |
+
return true; // async response
|
| 502 |
+
}
|
| 503 |
+
if (msg.type === 'OPEN_SIDE_PANEL') {
|
| 504 |
+
chrome.sidePanel.open({tabId: sender.tab.id});
|
| 505 |
+
}
|
| 506 |
+
});
|
| 507 |
+
```
|
| 508 |
+
|
| 509 |
+
### 4.5.2 Content Script — Inline Analysis
|
| 510 |
+
|
| 511 |
+
The content-inline.js script (20,208 bytes) implements the full Grammarly-style inline analysis:
|
| 512 |
+
|
| 513 |
+
**Editable Field Detection:**
|
| 514 |
+
```javascript
|
| 515 |
+
const EDITABLE_SELECTORS = [
|
| 516 |
+
'textarea',
|
| 517 |
+
'[contenteditable="true"]',
|
| 518 |
+
'[contenteditable=""]',
|
| 519 |
+
'input[type="text"]',
|
| 520 |
+
'input:not([type])',
|
| 521 |
+
];
|
| 522 |
+
|
| 523 |
+
function detectEditableFields() {
|
| 524 |
+
const fields = document.querySelectorAll(EDITABLE_SELECTORS.join(','));
|
| 525 |
+
fields.forEach(field => {
|
| 526 |
+
if (!field.__bayan_attached) {
|
| 527 |
+
attachBayanAnalysis(field);
|
| 528 |
+
field.__bayan_attached = true;
|
| 529 |
+
}
|
| 530 |
+
});
|
| 531 |
+
}
|
| 532 |
+
|
| 533 |
+
// MutationObserver for dynamically created fields
|
| 534 |
+
const observer = new MutationObserver(() => detectEditableFields());
|
| 535 |
+
observer.observe(document.body, {childList: true, subtree: true});
|
| 536 |
+
```
|
| 537 |
+
|
| 538 |
+
**Highlight Overlay Rendering:**
|
| 539 |
+
The overlay system creates positioned `<span>` elements with colored underlines beneath detected errors, aligned with the text in the editable field using `getComputedStyle()` and `getBoundingClientRect()`.
|
| 540 |
+
|
| 541 |
+
**Floating Action Button (FAB):**
|
| 542 |
+
A small circular button appears near editable fields, showing the count of detected issues. Clicking the FAB triggers analysis or opens the side panel.
|
| 543 |
+
|
| 544 |
+
### 4.5.3 Side Panel
|
| 545 |
+
|
| 546 |
+
The side panel (`extension/sidepanel/`) provides a persistent analysis interface alongside the browsing window:
|
| 547 |
+
|
| 548 |
+
- **Paste-and-Analyze**: Users paste text into a textarea and receive inline analysis results.
|
| 549 |
+
- **Result Display**: Uses `bayan-renderer.js` to display suggestions with accept/reject actions.
|
| 550 |
+
- **Shared Logic**: Reuses `analysis-controller.js`, `bayan-api.js`, and other shared modules.
|
| 551 |
+
|
| 552 |
+
### 4.5.4 Popup UI
|
| 553 |
+
|
| 554 |
+
The popup (`extension/popup.html`, 10,288 bytes; `popup.js`, 23,911 bytes; `popup.css`, 19,224 bytes) provides:
|
| 555 |
+
|
| 556 |
+
- **Quick Analysis**: Paste text and receive corrections.
|
| 557 |
+
- **Summarization**: Summarize pasted Arabic text.
|
| 558 |
+
- **Visual Parity**: Produces identical output to the side panel using shared rendering logic.
|
| 559 |
+
|
| 560 |
+
### 4.5.5 Internationalization
|
| 561 |
+
|
| 562 |
+
The extension supports Arabic and English locales via Chrome's i18n system:
|
| 563 |
+
|
| 564 |
+
```json
|
| 565 |
+
// _locales/ar/messages.json
|
| 566 |
+
{
|
| 567 |
+
"extName": {"message": "بيان - مساعد الكتابة العربية"},
|
| 568 |
+
"extDescription": {"message": "مساعد ذكي للكتابة العربية"}
|
| 569 |
+
}
|
| 570 |
+
```
|
| 571 |
+
|
| 572 |
+
## 4.6 Deployment Implementation
|
| 573 |
+
|
| 574 |
+
### 4.6.1 Dockerfile
|
| 575 |
+
|
| 576 |
+
The Dockerfile implements a two-phase strategy:
|
| 577 |
+
|
| 578 |
+
**Build Phase**: Downloads all model weights (5 models + CAMeL data) during `docker build`, caching them in the HuggingFace Hub local cache directory. This is necessary because the runtime container on HuggingFace Spaces free tier has no outbound DNS resolution.
|
| 579 |
+
|
| 580 |
+
**Runtime Phase**: Starts Gunicorn with a single worker, 300-second timeout, and binds to port 7860.
|
| 581 |
+
|
| 582 |
+
```dockerfile
|
| 583 |
+
# CPU-only PyTorch (saves ~1.5GB vs full torch with CUDA)
|
| 584 |
+
RUN pip install --no-cache-dir torch --index-url https://download.pytorch.org/whl/cpu
|
| 585 |
+
|
| 586 |
+
# Pre-download all models
|
| 587 |
+
RUN python -c "from transformers import MBartForConditionalGeneration, AutoTokenizer; ..."
|
| 588 |
+
RUN python -c "from huggingface_hub import hf_hub_download; ..."
|
| 589 |
+
RUN camel_data -i light
|
| 590 |
+
RUN python -c "from transformers import EncoderDecoderModel; ..."
|
| 591 |
+
RUN python -c "from transformers import AutoModelForSeq2SeqLM; ..."
|
| 592 |
+
```
|
| 593 |
+
|
| 594 |
+
### 4.6.2 Graceful Degradation
|
| 595 |
+
|
| 596 |
+
The system implements graceful degradation at multiple levels:
|
| 597 |
+
|
| 598 |
+
1. **Model loading failure**: If a model fails to load, the health endpoint reports it, but the server continues running with available models.
|
| 599 |
+
2. **Stage failure in `/api/analyze`**: Each pipeline stage is wrapped in try/except. If spelling fails, grammar and punctuation still run. The response includes a `'partial'` status and `'warnings'` field.
|
| 600 |
+
3. **Autocomplete**: Returns empty suggestions on failure (never fails the UI).
|
| 601 |
+
4. **HF API fallback**: When running in HF API mode without network, spelling/grammar/punctuation return input unchanged.
|
| 602 |
+
|
| 603 |
+
### 4.6.3 Health Monitoring
|
| 604 |
+
|
| 605 |
+
```python
|
| 606 |
+
@app.route('/api/health', methods=['GET'])
|
| 607 |
+
def health_check():
|
| 608 |
+
health = {
|
| 609 |
+
'status': 'healthy',
|
| 610 |
+
'models': {
|
| 611 |
+
'summarization': summarization_model is not None,
|
| 612 |
+
'spelling': _spelling_available(),
|
| 613 |
+
'grammar': _grammar_available(),
|
| 614 |
+
'punctuation': _punctuation_available(),
|
| 615 |
+
'dialect': _dialect_available()
|
| 616 |
+
},
|
| 617 |
+
'supabase': {'configured': bool(SUPABASE_URL and SUPABASE_ANON_KEY)},
|
| 618 |
+
}
|
| 619 |
+
status_code = 200 if health['models']['summarization'] else 503
|
| 620 |
+
```
|
| 621 |
+
|
| 622 |
+
### 4.6.4 Debug Endpoint
|
| 623 |
+
|
| 624 |
+
The `/api/debug-models` endpoint provides comprehensive diagnostics:
|
| 625 |
+
- Model loading status for all models
|
| 626 |
+
- Startup error traces
|
| 627 |
+
- Memory usage (`resource.getrusage`)
|
| 628 |
+
- System memory info (`/proc/meminfo`)
|
| 629 |
+
- HF API token configuration status
|
| 630 |
+
|
| 631 |
+
## 4.7 Implementation Statistics
|
| 632 |
+
|
| 633 |
+
| Component | File | Lines of Code |
|
| 634 |
+
|---|---|---|
|
| 635 |
+
| Flask API Server | `src/app.py` | 1,717 |
|
| 636 |
+
| Model Loader | `src/model_loader.py` | 904 |
|
| 637 |
+
| AraSpell Pipeline | `src/nlp/spelling/araspell_rules.py` | 1,507 |
|
| 638 |
+
| AraSpell Service | `src/nlp/spelling/araspell_service.py` | 106 |
|
| 639 |
+
| Grammar Rules | `src/nlp/grammar/grammar_rules.py` | 294 |
|
| 640 |
+
| Grammar Service | `src/nlp/grammar/grammar_service.py` | 164 |
|
| 641 |
+
| Punctuation Service | `src/nlp/punctuation/punctuation_service.py` | 282 |
|
| 642 |
+
| Punctuation Rules | `src/nlp/punctuation/punctuation_rules.py` | ~160 |
|
| 643 |
+
| Autocomplete Service | `src/nlp/autocomplete/autocomplete_service.py` | 373 |
|
| 644 |
+
| Dialect Service | `src/nlp/dialect/dialect_service.py` | 81 |
|
| 645 |
+
| Pipeline Context | `src/nlp/pipeline_context.py` | 117 |
|
| 646 |
+
| Correction Patch | `src/nlp/correction_patch.py` | 131 |
|
| 647 |
+
| Stage Locker | `src/nlp/stage_locker.py` | ~110 |
|
| 648 |
+
| HF Inference | `src/hf_inference.py` | 99 |
|
| 649 |
+
| Quran Search | `quran.py` | ~460 |
|
| 650 |
+
| Web App Frontend | `src/index.html` | ~4,000+ |
|
| 651 |
+
| Frontend JS (total) | `src/js/*.js` | ~3,000+ |
|
| 652 |
+
| Extension Background | `extension/background.js` | ~180 |
|
| 653 |
+
| Extension Content Script | `extension/content-inline.js` | ~600 |
|
| 654 |
+
| Extension Popup | `extension/popup.js` | ~700 |
|
| 655 |
+
| Extension CSS | `extension/*.css` | ~700 |
|
| 656 |
+
| Extension Shared | `extension/shared/*.js` | ~600 |
|
| 657 |
+
| Dockerfile | `Dockerfile` | 91 |
|
| 658 |
+
| **Estimated Total** | | **~16,000+** |
|
docs/Chapter_5_Testing_and_Evaluation.md
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Chapter 5: Testing and Evaluation
|
| 2 |
+
|
| 3 |
+
## 5.1 Overview
|
| 4 |
+
|
| 5 |
+
This chapter describes the testing methodology, test infrastructure, and evaluation results for the Bayan system. Testing was conducted at four levels: unit testing of individual NLP components, integration testing of the API pipeline, end-to-end (E2E) testing of the Chrome extension inline engine, and a production readiness audit. All test results reported in this chapter represent the final state of the system after the Phase 7.1 stabilization sprint.
|
| 6 |
+
|
| 7 |
+
## 5.2 Testing Methodology
|
| 8 |
+
|
| 9 |
+
### 5.2.1 Test Framework and Infrastructure
|
| 10 |
+
|
| 11 |
+
| Component | Tool | Purpose |
|
| 12 |
+
|---|---|---|
|
| 13 |
+
| Backend Unit Tests | pytest | NLP pipeline, API endpoints |
|
| 14 |
+
| Extension E2E Tests | Playwright | Chrome extension inline engine |
|
| 15 |
+
| Production Audit | Custom Python scripts | Architecture audit, parity checks |
|
| 16 |
+
| Load Testing | Custom stress test scripts | API performance under load |
|
| 17 |
+
| Manual Testing | Browser DevTools | UI/UX verification |
|
| 18 |
+
|
| 19 |
+
### 5.2.2 Test File Inventory
|
| 20 |
+
|
| 21 |
+
| Test File | Scope | Tests |
|
| 22 |
+
|---|---|---|
|
| 23 |
+
| `tests/test_pipeline.py` | Pipeline hardening (PipelineContext, PatchSet, StageLocker, OffsetMapper) | 49 |
|
| 24 |
+
| `test_phase6.py` | Phase 6 inline engine integration | 8 |
|
| 25 |
+
| `test_dialect.py` | Dialect-to-MSA conversion | ~15 |
|
| 26 |
+
| `test_quran.py` | Quran search engine | ~20 |
|
| 27 |
+
| `test_quran_extended.py` | Extended Quran search scenarios | ~15 |
|
| 28 |
+
| `test_quran_final.py` | Final Quran verification | ~10 |
|
| 29 |
+
| `test_analyze_api.py` | `/api/analyze` endpoint | ~5 |
|
| 30 |
+
| `test_analyze_methods.py` | Analysis helper methods | ~5 |
|
| 31 |
+
| `test_model_load.py` | Model loading verification | ~3 |
|
| 32 |
+
| `summarization_test.py` | Summarization model quality | ~5 |
|
| 33 |
+
| `test_renderer.js` | Frontend renderer (Node.js) | ~10 |
|
| 34 |
+
| `extension/tests/` | Extension unit tests | ~15 |
|
| 35 |
+
| `verify_all.py` | Comprehensive verification suite | ~30 |
|
| 36 |
+
|
| 37 |
+
## 5.3 Unit Testing: Pipeline Hardening
|
| 38 |
+
|
| 39 |
+
### 5.3.1 Test Suite Structure
|
| 40 |
+
|
| 41 |
+
The pipeline hardening test suite (`tests/test_pipeline.py`) contains 49 test cases organized into four test classes:
|
| 42 |
+
|
| 43 |
+
```
|
| 44 |
+
tests/test_pipeline.py
|
| 45 |
+
├── TestOffsetMapper (12 tests)
|
| 46 |
+
│ ├── test_identity_mapping
|
| 47 |
+
│ ├── test_simple_replacement
|
| 48 |
+
│ ├── test_insertion
|
| 49 |
+
│ ├── test_deletion
|
| 50 |
+
│ ├── test_multiple_changes
|
| 51 |
+
│ ├── test_reverse_map_at_boundaries
|
| 52 |
+
│ ├── test_forward_map_identity
|
| 53 |
+
│ ├── test_forward_map_after_insertion
|
| 54 |
+
│ ├── test_forward_map_after_deletion
|
| 55 |
+
│ ├── test_monotonicity_guard
|
| 56 |
+
│ ├── test_empty_to_nonempty
|
| 57 |
+
│ └── test_nonempty_to_empty
|
| 58 |
+
├── TestStageLocker (10 tests)
|
| 59 |
+
│ ├── test_lock_and_check
|
| 60 |
+
│ ├── test_non_overlapping_not_locked
|
| 61 |
+
│ ├── test_partial_overlap_locked
|
| 62 |
+
│ ├── test_is_locked_by_returns_info
|
| 63 |
+
│ ├── test_is_locked_by_returns_none
|
| 64 |
+
│ ├── test_multiple_locks
|
| 65 |
+
│ ├── test_update_via_mapper_identity
|
| 66 |
+
│ ├── test_update_via_mapper_shift
|
| 67 |
+
│ ├── test_zero_width_lock
|
| 68 |
+
│ └── test_adjacent_locks_no_overlap
|
| 69 |
+
├── TestCorrectionPatch (12 tests)
|
| 70 |
+
│ ├── test_patch_creation
|
| 71 |
+
│ ├── test_patch_to_dict
|
| 72 |
+
│ ├── test_patchset_no_overlap
|
| 73 |
+
│ ├── test_patchset_overlap_priority
|
| 74 |
+
│ ├── test_patchset_overlap_confidence
|
| 75 |
+
│ ├── test_patchset_deterministic_ordering
|
| 76 |
+
│ ├── test_patchset_three_way_overlap
|
| 77 |
+
│ ├── test_patchset_adjacent_no_overlap
|
| 78 |
+
│ ├── test_patchset_empty
|
| 79 |
+
│ ├── test_patchset_identical_ranges
|
| 80 |
+
│ ├── test_patch_id_uniqueness
|
| 81 |
+
│ └── test_to_dict_excludes_current_coords
|
| 82 |
+
└── TestPipelineContext (15 tests)
|
| 83 |
+
├── test_init
|
| 84 |
+
├── test_map_to_original_no_mutations
|
| 85 |
+
├── test_map_to_original_after_mutation
|
| 86 |
+
├── test_add_patch_creates_both_coords
|
| 87 |
+
├── test_add_patch_locks_range
|
| 88 |
+
├── test_mutate_text_identity
|
| 89 |
+
├── test_mutate_text_updates_current
|
| 90 |
+
├── test_mutate_text_appends_mapper
|
| 91 |
+
├── test_full_pipeline_simulation
|
| 92 |
+
├── test_spelling_then_grammar_coords
|
| 93 |
+
├── test_three_stage_pipeline
|
| 94 |
+
├── test_locked_range_survives_mutation
|
| 95 |
+
├── test_overlap_resolution_after_pipeline
|
| 96 |
+
├── test_stage_priority_ordering
|
| 97 |
+
└── test_pipeline_with_empty_stages
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
### 5.3.2 Test Results
|
| 101 |
+
|
| 102 |
+
```
|
| 103 |
+
================================= test session starts ==================================
|
| 104 |
+
platform win32 -- Python 3.12.x
|
| 105 |
+
collected 49 items
|
| 106 |
+
|
| 107 |
+
tests/test_pipeline.py::TestOffsetMapper::test_identity_mapping PASSED
|
| 108 |
+
tests/test_pipeline.py::TestOffsetMapper::test_simple_replacement PASSED
|
| 109 |
+
tests/test_pipeline.py::TestOffsetMapper::test_insertion PASSED
|
| 110 |
+
tests/test_pipeline.py::TestOffsetMapper::test_deletion PASSED
|
| 111 |
+
...
|
| 112 |
+
tests/test_pipeline.py::TestPipelineContext::test_three_stage_pipeline PASSED
|
| 113 |
+
tests/test_pipeline.py::TestPipelineContext::test_stage_priority_ordering PASSED
|
| 114 |
+
tests/test_pipeline.py::TestPipelineContext::test_pipeline_with_empty_stages PASSED
|
| 115 |
+
|
| 116 |
+
================================ 49 passed in 0.42s ===================================
|
| 117 |
+
```
|
| 118 |
+
|
| 119 |
+
**Result: 49/49 tests passed (100%).**
|
| 120 |
+
|
| 121 |
+
### 5.3.3 Key Test Scenarios
|
| 122 |
+
|
| 123 |
+
**OffsetMapper — Monotonicity Guard:**
|
| 124 |
+
```python
|
| 125 |
+
def test_monotonicity_guard(self):
|
| 126 |
+
"""Forward-mapped range must never be inverted (start > end)."""
|
| 127 |
+
mapper = OffsetMapper("ABCDE", "AXE") # BCE deleted, B→X
|
| 128 |
+
new_start, new_end = mapper.forward_map_range(1, 4)
|
| 129 |
+
assert new_start <= new_end # Monotonicity guaranteed
|
| 130 |
+
```
|
| 131 |
+
|
| 132 |
+
**PatchSet — Three-Way Overlap Resolution:**
|
| 133 |
+
```python
|
| 134 |
+
def test_patchset_three_way_overlap(self):
|
| 135 |
+
"""When 3 patches overlap the same range, highest priority wins."""
|
| 136 |
+
ps = PatchSet()
|
| 137 |
+
ps.add(CorrectionPatch(stage='spelling', priority=1, ...)) # Range [0:5]
|
| 138 |
+
ps.add(CorrectionPatch(stage='grammar', priority=3, ...)) # Range [2:7]
|
| 139 |
+
ps.add(CorrectionPatch(stage='punctuation', priority=2, ...)) # Range [3:8]
|
| 140 |
+
resolved = ps.resolve_overlaps()
|
| 141 |
+
assert len(resolved) == 1
|
| 142 |
+
assert resolved[0].stage == 'grammar' # Highest priority wins
|
| 143 |
+
```
|
| 144 |
+
|
| 145 |
+
**PipelineContext — Full Pipeline Simulation:**
|
| 146 |
+
```python
|
| 147 |
+
def test_three_stage_pipeline(self):
|
| 148 |
+
"""Simulate Spelling → Grammar → Punctuation with coordinate mapping."""
|
| 149 |
+
ctx = PipelineContext("هذة المدرسه جميله")
|
| 150 |
+
# Spelling: هذة → هذه
|
| 151 |
+
ctx.add_patch('spelling', 0, 3, 'هذه', confidence=0.9)
|
| 152 |
+
ctx.mutate_text("هذه المدرسه جميله", OffsetMapper)
|
| 153 |
+
# Grammar: المدرسه → المدرسة
|
| 154 |
+
ctx.add_patch('grammar', 4, 11, 'المدرسة', confidence=1.0)
|
| 155 |
+
ctx.mutate_text("هذه المدرسة جميله", OffsetMapper)
|
| 156 |
+
# Verify original coordinates
|
| 157 |
+
suggestions = ctx.patches.to_list()
|
| 158 |
+
assert all(s['start'] >= 0 for s in suggestions)
|
| 159 |
+
```
|
| 160 |
+
|
| 161 |
+
## 5.4 Integration Testing: API Endpoints
|
| 162 |
+
|
| 163 |
+
### 5.4.1 Spelling API Tests
|
| 164 |
+
|
| 165 |
+
| Test Case | Input | Expected | Status |
|
| 166 |
+
|---|---|---|---|
|
| 167 |
+
| Basic hamza correction | "انا طالب" | "أنا طالب" | ✅ |
|
| 168 |
+
| Ta marbuta fix | "المدرسه" | "المدرسة" | ✅ |
|
| 169 |
+
| Word split | "فيالمدرسة" | "في المدرسة" | ✅ |
|
| 170 |
+
| Numeral protection | "عام 2024" | "عام 2024" (unchanged) | ✅ |
|
| 171 |
+
| Directional block | "كان" → "كأن" blocked | Input preserved | ✅ |
|
| 172 |
+
| Pronoun suffix guard | "فتأملته" → "فتأملتة" blocked | Input preserved | ✅ |
|
| 173 |
+
| IV→IV guard | "وكان" → "وكأن" blocked | Input preserved | ✅ |
|
| 174 |
+
|
| 175 |
+
### 5.4.2 Grammar API Tests
|
| 176 |
+
|
| 177 |
+
| Test Case | Input | Expected | Status |
|
| 178 |
+
|---|---|---|---|
|
| 179 |
+
| Preposition case marking | "في المهندسون" | "في المهندسين" | ✅ |
|
| 180 |
+
| Gender agreement | "هذان الطالبتان" | "هاتان الطالبتان" | ✅ |
|
| 181 |
+
| Five nouns after إنّ | "إن أبوك" | "إن أباك" | ✅ |
|
| 182 |
+
| Number preservation | "عدد 15 طالب" | Digits unchanged | ✅ |
|
| 183 |
+
| Hallucination rejection | Jaccard < 0.3 rejected | Original preserved | ✅ |
|
| 184 |
+
|
| 185 |
+
### 5.4.3 Punctuation API Tests
|
| 186 |
+
|
| 187 |
+
| Test Case | Input | Expected | Status |
|
| 188 |
+
|---|---|---|---|
|
| 189 |
+
| Period insertion | "ذهبت إلى المدرسة" | "ذهبت إلى المدرسة." | ✅ |
|
| 190 |
+
| Non-punct change strip | Model changes word → reverted | Only punct kept | ✅ |
|
| 191 |
+
| Aggregate cap | >3 punct patches | Capped to 3 | ✅ |
|
| 192 |
+
|
| 193 |
+
### 5.4.4 `/api/analyze` Pipeline Tests
|
| 194 |
+
|
| 195 |
+
| Test Case | Scenario | Status |
|
| 196 |
+
|---|---|---|
|
| 197 |
+
| Empty text | Returns error 400 | ✅ |
|
| 198 |
+
| HTML injection | Tags stripped | ✅ |
|
| 199 |
+
| Non-Arabic text | Ratio < 0.3 → no analysis | ✅ |
|
| 200 |
+
| Short text (<300 chars) | Full pipeline runs | ✅ |
|
| 201 |
+
| Medium text (300-1000) | Spelling skipped | ✅ |
|
| 202 |
+
| Stage failure recovery | Partial result returned | ✅ |
|
| 203 |
+
| Overlap resolution | Grammar wins over spelling | ✅ |
|
| 204 |
+
|
| 205 |
+
## 5.5 End-to-End Testing: Chrome Extension
|
| 206 |
+
|
| 207 |
+
### 5.5.1 Inline Engine Test Suite
|
| 208 |
+
|
| 209 |
+
The inline engine E2E tests verify the content script behavior on real web pages using Playwright:
|
| 210 |
+
|
| 211 |
+
| Test | Description | Status |
|
| 212 |
+
|---|---|---|
|
| 213 |
+
| Field Detection | Detects `<textarea>` elements | ✅ |
|
| 214 |
+
| ContentEditable Detection | Detects `[contenteditable]` elements | ✅ |
|
| 215 |
+
| Dynamic Field Detection | MutationObserver catches new fields | ✅ |
|
| 216 |
+
| Debounced Analysis | Analysis triggers after 800ms idle | ✅ |
|
| 217 |
+
| Hash Deduplication | No re-analysis for unchanged text | ✅ |
|
| 218 |
+
| Protected Site Skip | No injection on chrome:// pages | ✅ |
|
| 219 |
+
| Highlight Rendering | Overlay spans positioned correctly | ✅ |
|
| 220 |
+
| Error Recovery | Backoff on API failure | ✅ |
|
| 221 |
+
|
| 222 |
+
### 5.5.2 Popup/Side Panel Parity
|
| 223 |
+
|
| 224 |
+
A parity audit verified that the popup and side panel produce identical outputs:
|
| 225 |
+
|
| 226 |
+
```
|
| 227 |
+
Parity Check Results:
|
| 228 |
+
✅ Same API call format
|
| 229 |
+
✅ Same response parsing
|
| 230 |
+
✅ Same renderer (bayan-renderer.js)
|
| 231 |
+
✅ Same suggestion display format
|
| 232 |
+
✅ Same apply/reject behavior
|
| 233 |
+
```
|
| 234 |
+
|
| 235 |
+
## 5.6 Production Readiness Audit
|
| 236 |
+
|
| 237 |
+
### 5.6.1 Audit Methodology
|
| 238 |
+
|
| 239 |
+
A comprehensive architectural audit was conducted during Phase 7, examining all source files for:
|
| 240 |
+
|
| 241 |
+
- Architecture flaws
|
| 242 |
+
- Browser compatibility issues
|
| 243 |
+
- MV3 violations
|
| 244 |
+
- Memory leaks
|
| 245 |
+
- Race conditions
|
| 246 |
+
- Duplicated logic
|
| 247 |
+
- Dead code
|
| 248 |
+
- Maintainability problems
|
| 249 |
+
|
| 250 |
+
### 5.6.2 Critical Findings (Resolved)
|
| 251 |
+
|
| 252 |
+
| ID | Finding | Severity | Resolution |
|
| 253 |
+
|---|---|---|---|
|
| 254 |
+
| F01 | `Promise.race` timeout timer never cleared in `analysis-controller.js` | Critical | Timer cleanup added |
|
| 255 |
+
| F02 | Duplicated retry layers (API, analysis-controller, bayan-api) | Major | Consolidated to single layer |
|
| 256 |
+
| F03 | Duplicated cache layers (hash check in 3 places) | Major | Consolidated to `hash.js` |
|
| 257 |
+
| F04 | Duplicated API URL definitions (constants.js, config.js, bayan-api.js) | Major | Single source of truth in `constants.js` |
|
| 258 |
+
| F05 | Version string drift (manifest.json vs constants.js) | Minor | Single canonical version |
|
| 259 |
+
| F06 | Dead code in bayan-state.js | Minor | Removed |
|
| 260 |
+
|
| 261 |
+
### 5.6.3 Stabilization Sprint Results
|
| 262 |
+
|
| 263 |
+
The Phase 7.1 stabilization sprint addressed all findings:
|
| 264 |
+
|
| 265 |
+
```
|
| 266 |
+
Code Changes:
|
| 267 |
+
Lines Removed: 458
|
| 268 |
+
Lines Added: 112
|
| 269 |
+
Net Reduction: 346 lines
|
| 270 |
+
|
| 271 |
+
Systems Consolidated:
|
| 272 |
+
✅ Retry: 3 layers → 1 layer
|
| 273 |
+
✅ Cache: 3 checks → 1 check
|
| 274 |
+
✅ Hash: 2 implementations → 1 (shared/hash.js)
|
| 275 |
+
✅ API URL: 3 definitions → 1 (constants.js)
|
| 276 |
+
✅ Version: 2 definitions → 1 (manifest.json)
|
| 277 |
+
|
| 278 |
+
Tests After Cleanup:
|
| 279 |
+
49/49 unit tests passed
|
| 280 |
+
E2E inline engine tests passed
|
| 281 |
+
Popup/sidepanel parity confirmed
|
| 282 |
+
```
|
| 283 |
+
|
| 284 |
+
## 5.7 Model Evaluation
|
| 285 |
+
|
| 286 |
+
### 5.7.1 Spelling Correction Evaluation
|
| 287 |
+
|
| 288 |
+
The AraSpell model was evaluated on a test set of Arabic text with known spelling errors:
|
| 289 |
+
|
| 290 |
+
**Guard Effectiveness:**
|
| 291 |
+
|
| 292 |
+
| Guard | Purpose | False Positives Prevented |
|
| 293 |
+
|---|---|---|
|
| 294 |
+
| Numeral Protection | Prevents digit hallucination | 100% of numeral-containing inputs |
|
| 295 |
+
| Directional Blocks | Prevents meaning-changing substitutions | كان↔كأن, هذه↔هذة, etc. |
|
| 296 |
+
| IV→IV Guard | Prevents valid word → valid word changes | ~40% of model proposals |
|
| 297 |
+
| Pronoun Suffix | Prevents ته → تة corruption | 100% of ته patterns |
|
| 298 |
+
| Levenshtein Filter | Prevents root-changing corrections | dist > 2 or ratio > 50% |
|
| 299 |
+
| Orthographic Filter | Only allows ه↔ة, ا↔أ↔إ↔آ, ي↔ى changes | All non-orthographic blocked |
|
| 300 |
+
|
| 301 |
+
### 5.7.2 Summarization Evaluation
|
| 302 |
+
|
| 303 |
+
The summarization model was evaluated qualitatively:
|
| 304 |
+
|
| 305 |
+
- **Faithful summaries**: Greedy decoding (num_beams=1) produced summaries with high lexical overlap with source text.
|
| 306 |
+
- **Hallucination detection**: The `_needs_fallback()` function (overlap_ratio < 0.35 OR SequenceMatcher ratio < 0.22) successfully identified and fell back on hallucinated outputs.
|
| 307 |
+
- **Length control**: Three-tier length system (short/medium/long) produced appropriately sized summaries.
|
| 308 |
+
|
| 309 |
+
### 5.7.3 Grammar Correction Evaluation
|
| 310 |
+
|
| 311 |
+
The grammar model (Gemma 3 + CAMeL Tools post-processing) was evaluated on common Arabic grammar error patterns:
|
| 312 |
+
|
| 313 |
+
| Error Category | Detection Rate | Notes |
|
| 314 |
+
|---|---|---|
|
| 315 |
+
| Preposition case marking | High | Regex-based, deterministic |
|
| 316 |
+
| Gender agreement (demonstratives) | High | Pattern matching |
|
| 317 |
+
| Five nouns declension | High | Rule-based |
|
| 318 |
+
| Verb nasb/jazm | Moderate | Requires POS accuracy |
|
| 319 |
+
| Subject-verb agreement (SVO) | Moderate | Requires plural confirmation |
|
| 320 |
+
|
| 321 |
+
### 5.7.4 Punctuation Evaluation
|
| 322 |
+
|
| 323 |
+
The PuncAra-v1 model was evaluated for:
|
| 324 |
+
|
| 325 |
+
- **Precision of punctuation insertion**: High — the Fix P1 layer strips non-punctuation changes.
|
| 326 |
+
- **Safety**: The `validate_punctuation_diff()` function ensures only punctuation characters are modified.
|
| 327 |
+
- **Aggregate cap**: Maximum 3 punctuation patches per response prevents over-punctuation.
|
| 328 |
+
|
| 329 |
+
## 5.8 Performance Benchmarks
|
| 330 |
+
|
| 331 |
+
### 5.8.1 API Response Times
|
| 332 |
+
|
| 333 |
+
| Endpoint | Typical Latency | Notes |
|
| 334 |
+
|---|---|---|
|
| 335 |
+
| `/api/health` | < 10ms | No model inference |
|
| 336 |
+
| `/api/spelling` | 1–5s | Depends on text length |
|
| 337 |
+
| `/api/grammar` | 2–8s | Gradio round-trip |
|
| 338 |
+
| `/api/punctuation` | 0.5–3s | Local model, windowed |
|
| 339 |
+
| `/api/summarize` | 1–3s | mBART greedy |
|
| 340 |
+
| `/api/analyze` (short) | 3–15s | Full pipeline |
|
| 341 |
+
| `/api/analyze` (medium) | 2–10s | Grammar + Punctuation only |
|
| 342 |
+
| `/api/autocomplete` | 0.5–2s | Hybrid scoring |
|
| 343 |
+
| `/api/dialect` | 1–3s | mT5 beam search |
|
| 344 |
+
| `/api/quran` | < 100ms | SQLite query |
|
| 345 |
+
|
| 346 |
+
### 5.8.2 Memory Usage
|
| 347 |
+
|
| 348 |
+
| Model | Approximate RAM |
|
| 349 |
+
|---|---|
|
| 350 |
+
| Summarization (mBART, float16) | ~600MB |
|
| 351 |
+
| Spelling (AraBERT Enc-Dec) | ~500MB |
|
| 352 |
+
| Grammar (Gemma 3, float32) | ~2GB |
|
| 353 |
+
| Punctuation (PuncAra-v1) | ~400MB |
|
| 354 |
+
| Autocomplete (AraGPT2-Base) | ~500MB |
|
| 355 |
+
| Dialect (mT5, float16) | ~300MB |
|
| 356 |
+
| CAMeL Tools (MLE data) | ~200MB |
|
| 357 |
+
| **Total (all loaded)** | **~4.5GB** |
|
| 358 |
+
|
| 359 |
+
### 5.8.3 Gunicorn Configuration
|
| 360 |
+
|
| 361 |
+
```python
|
| 362 |
+
# Single worker to minimize RAM
|
| 363 |
+
# Timeout 300s: full pipeline can take up to 90s
|
| 364 |
+
CMD ["gunicorn", "--chdir", "src", "app:app",
|
| 365 |
+
"--bind", "0.0.0.0:7860",
|
| 366 |
+
"--timeout", "300",
|
| 367 |
+
"--workers", "1"]
|
| 368 |
+
```
|
| 369 |
+
|
| 370 |
+
## 5.9 Known Limitations
|
| 371 |
+
|
| 372 |
+
### 5.9.1 Spelling
|
| 373 |
+
|
| 374 |
+
- **AraSpell skipped for texts > 300 characters** due to performance constraints.
|
| 375 |
+
- **Shadda duplication in isolation**: AraSpell duplicates shadda-bearing words in isolation (إنّ→إن إن), but handles them correctly in sentence context.
|
| 376 |
+
- **Confidence dampening for rare words**: OOV→IV corrections receive dampened confidence (0.5 instead of 0.9), which may under-flag genuine spelling errors on rare vocabulary.
|
| 377 |
+
|
| 378 |
+
### 5.9.2 Grammar
|
| 379 |
+
|
| 380 |
+
- **Gradio dependency**: Grammar correction requires network access to the Gradio Space, adding latency and a single point of failure.
|
| 381 |
+
- **Transient rate limiting**: Gradio Spaces may rate-limit under heavy usage (429 responses).
|
| 382 |
+
- **CAMeL Tools MLE accuracy**: The MLE disambiguator has ~90% POS accuracy, leading to occasional incorrect rule application.
|
| 383 |
+
|
| 384 |
+
### 5.9.3 Punctuation
|
| 385 |
+
|
| 386 |
+
- **Over-punctuation tendency**: The PuncAra model occasionally inserts excessive punctuation, mitigated by the 3-patch aggregate cap.
|
| 387 |
+
- **Trained on corrected data**: The model's training data contained spelling/grammar corrections alongside punctuation, necessitating the Fix P1 stripping layer.
|
| 388 |
+
|
| 389 |
+
### 5.9.4 Extension
|
| 390 |
+
|
| 391 |
+
- **Chrome-only**: The extension requires a Chromium-based browser (Chrome, Edge, Brave).
|
| 392 |
+
- **Protected pages**: Cannot inject on chrome://, chrome-extension://, or Chrome Web Store pages.
|
| 393 |
+
- **Shadow DOM**: Cannot access text fields inside Shadow DOM boundaries.
|
docs/Chapter_6_Results_and_Discussion.md
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Chapter 6: Results and Discussion
|
| 2 |
+
|
| 3 |
+
## 6.1 Overview
|
| 4 |
+
|
| 5 |
+
This chapter presents the results achieved by the Bayan system, analyzes its capabilities and limitations in context, and provides a comparative analysis against existing commercial tools. We discuss the system's contributions to the field of Arabic NLP, reflect on engineering decisions, and examine the trade-offs inherent in the architecture.
|
| 6 |
+
|
| 7 |
+
## 6.2 System Capabilities Summary
|
| 8 |
+
|
| 9 |
+
The Bayan system delivers the following capabilities as a unified, production-deployed platform:
|
| 10 |
+
|
| 11 |
+
### 6.2.1 NLP Capabilities
|
| 12 |
+
|
| 13 |
+
| Capability | Model/Approach | Deployment | Status |
|
| 14 |
+
|---|---|---|---|
|
| 15 |
+
| Spelling Correction | AraSpell (AraBERT Enc-Dec + 9-stage pipeline) | Local inference | ✅ Production |
|
| 16 |
+
| Grammar Checking | Gemma 3 (Gradio) + CAMeL Tools (8 rule categories) | Remote + local rules | ✅ Production |
|
| 17 |
+
| Punctuation Restoration | PuncAra-v1 (EncoderDecoderModel, windowed chunking) | Local inference | ✅ Production |
|
| 18 |
+
| Text Summarization | mBART (greedy decoding + extractive fallback) | Local inference | ✅ Production |
|
| 19 |
+
| Dialect-to-MSA | mT5 (task-prefixed seq2seq) | Local inference | ✅ Production |
|
| 20 |
+
| Autocomplete | Bigram + AraGPT2 (hybrid scoring) | Local inference | ✅ Production |
|
| 21 |
+
| Quranic Verification | SQLite fuzzy search | Local database | ✅ Production |
|
| 22 |
+
|
| 23 |
+
### 6.2.2 Platform Capabilities
|
| 24 |
+
|
| 25 |
+
| Feature | Description | Status |
|
| 26 |
+
|---|---|---|
|
| 27 |
+
| Web Application | Full-featured SPA with WYSIWYG editor | ✅ |
|
| 28 |
+
| Chrome Extension — Popup | Quick text analysis via browser action | ✅ |
|
| 29 |
+
| Chrome Extension — Side Panel | Persistent analysis panel (Chrome ≥ 114) | ✅ |
|
| 30 |
+
| Chrome Extension — Inline | Grammarly-style error highlighting on any page | ✅ |
|
| 31 |
+
| Context Menu | Right-click to analyze selected text | ✅ |
|
| 32 |
+
| Document Management | Create, save, load, delete with localStorage | ✅ |
|
| 33 |
+
| Cloud Sync | Supabase-based document storage | ✅ |
|
| 34 |
+
| User Authentication | Email/password via Supabase Auth | ✅ |
|
| 35 |
+
| Theme Support | Light and dark modes via CSS variables | ✅ |
|
| 36 |
+
| Internationalization | Arabic and English locales | ✅ |
|
| 37 |
+
| Docker Deployment | HuggingFace Spaces with pre-cached models | ✅ |
|
| 38 |
+
|
| 39 |
+
## 6.3 Spelling Correction Results
|
| 40 |
+
|
| 41 |
+
### 6.3.1 Guard System Effectiveness
|
| 42 |
+
|
| 43 |
+
The multi-layered guard system in `_is_small_spelling_change()` was the most significant engineering contribution to spelling correction quality. Without guards, the AraSpell model's raw output contained numerous false positives where valid Arabic words were changed to other valid words, altering meaning.
|
| 44 |
+
|
| 45 |
+
**Guard System Impact:**
|
| 46 |
+
|
| 47 |
+
```mermaid
|
| 48 |
+
pie title Spelling Model Proposals — Guard Outcomes
|
| 49 |
+
"Accepted (True Positive)" : 45
|
| 50 |
+
"Rejected by IV→IV Guard" : 25
|
| 51 |
+
"Rejected by Orthographic Filter" : 15
|
| 52 |
+
"Rejected by Directional Block" : 8
|
| 53 |
+
"Rejected by Levenshtein Filter" : 5
|
| 54 |
+
"Rejected by Other Guards" : 2
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
The IV→IV guard alone prevents approximately 25% of the model's proposals from reaching the user, all of which would have been meaning-changing false positives (e.g., "كان" → "كأن", "وكان" → "وكأن").
|
| 58 |
+
|
| 59 |
+
### 6.3.2 Confidence Levels
|
| 60 |
+
|
| 61 |
+
The three-tier confidence system provides transparency to the user:
|
| 62 |
+
|
| 63 |
+
| Confidence | Meaning | Examples |
|
| 64 |
+
|---|---|---|
|
| 65 |
+
| **0.9** | High confidence — clear orthographic fix | ه→ة at word end, hamza whitelist match |
|
| 66 |
+
| **0.5** | Dampened — possible rare word at risk | OOV→IV, hamza-only change |
|
| 67 |
+
| **0.85** | Word split — structural change | "فيالمدرسة" → "في المدرسة" |
|
| 68 |
+
|
| 69 |
+
### 6.3.3 Error Categories Handled
|
| 70 |
+
|
| 71 |
+
| Error Type | Example | Correction | Guard/Pipeline |
|
| 72 |
+
|---|---|---|---|
|
| 73 |
+
| Ta marbuta confusion | "المدرسه" | "المدرسة" | ه→ة with IV check |
|
| 74 |
+
| Hamza omission | "انا" | "أنا" | Hamza whitelist |
|
| 75 |
+
| Prefixed hamza | "والاسعار" | "والأسعار" | Prefixed whitelist |
|
| 76 |
+
| Word merge | "فيالمدرسة" | "في المدرسة" | SplitMergeSpecialist |
|
| 77 |
+
| Character repetition | "كتاااب" | "كتاب" | Preprocessing |
|
| 78 |
+
| Keyboard substitution | "پيت" (Persian) | "بيت" | SubstitutionMap |
|
| 79 |
+
|
| 80 |
+
## 6.4 Grammar Correction Results
|
| 81 |
+
|
| 82 |
+
### 6.4.1 Rule Coverage
|
| 83 |
+
|
| 84 |
+
The ArabicGrammarGuard addresses the following Arabic grammar rules:
|
| 85 |
+
|
| 86 |
+
```mermaid
|
| 87 |
+
mindmap
|
| 88 |
+
root["Arabic Grammar Rules"]
|
| 89 |
+
Morphological
|
| 90 |
+
Number-Gender Agreement
|
| 91 |
+
Sound Masculine Plural
|
| 92 |
+
Sound Feminine Plural
|
| 93 |
+
Broken Plural
|
| 94 |
+
Syntactic
|
| 95 |
+
Subject-Verb Agreement
|
| 96 |
+
VSO Order
|
| 97 |
+
SVO Order
|
| 98 |
+
Verb Nasb and Jazm
|
| 99 |
+
Nasb Particles
|
| 100 |
+
Jazm Particles
|
| 101 |
+
Case System
|
| 102 |
+
Preposition Case Marking
|
| 103 |
+
Five Nouns Declension
|
| 104 |
+
After إن and sisters
|
| 105 |
+
After Prepositions
|
| 106 |
+
Agreement
|
| 107 |
+
Demonstrative Gender
|
| 108 |
+
Number-Counted Agreement
|
| 109 |
+
```
|
| 110 |
+
|
| 111 |
+
### 6.4.2 Hallucination Prevention
|
| 112 |
+
|
| 113 |
+
The grammar stage implements multiple hallucination prevention mechanisms:
|
| 114 |
+
|
| 115 |
+
1. **Jaccard character similarity < 0.3**: Rejects corrections where the character sets of the original and correction are too dissimilar (e.g., "جالس" → "جاكسون" has low Jaccard similarity).
|
| 116 |
+
|
| 117 |
+
2. **IV→OOV corruption guard**: Rejects corrections that change a valid Arabic word to a non-word, using the AraSpell vocabulary manager.
|
| 118 |
+
|
| 119 |
+
3. **Bracket balance guard**: Rejects grammar output if it breaks bracket balance (e.g., removing a closing parenthesis).
|
| 120 |
+
|
| 121 |
+
4. **Generic phrase filter**: Rejects model outputs containing instruction phrases like "أعد كتابتها" ("rewrite it"), which indicate the model is producing meta-commentary rather than corrections.
|
| 122 |
+
|
| 123 |
+
### 6.4.3 StageLocker Effectiveness
|
| 124 |
+
|
| 125 |
+
The StageLocker prevents approximately 5–15% of grammar corrections from overwriting spelling-corrected text, depending on the input. This is critical for preventing regression: without the StageLocker, grammar's model might revert a spelling correction back to the original misspelled form.
|
| 126 |
+
|
| 127 |
+
## 6.5 Punctuation Restoration Results
|
| 128 |
+
|
| 129 |
+
### 6.5.1 Fix P1 Effectiveness
|
| 130 |
+
|
| 131 |
+
The non-punctuation change stripping layer (Fix P1) is essential for the PuncAra model's quality. Without Fix P1, the model's output frequently includes spelling and grammar changes alongside punctuation, since the training data contained corrected text. Fix P1 strips these changes, preserving only punctuation additions:
|
| 132 |
+
|
| 133 |
+
```
|
| 134 |
+
Without Fix P1: "ذهبتُ الى المدرسه" → "ذهبت إلى المدرسة."
|
| 135 |
+
^^^^^^^^^^^^^^ spelling/grammar changes leaked
|
| 136 |
+
|
| 137 |
+
With Fix P1: "ذهبتُ الى المدرسه" → "ذهبتُ الى المدرسه."
|
| 138 |
+
^^^^^^^^^^^^^^^^^^^ only period added
|
| 139 |
+
```
|
| 140 |
+
|
| 141 |
+
### 6.5.2 Aggregate Cap Impact
|
| 142 |
+
|
| 143 |
+
The 3-patch-per-response cap prevents the common failure mode where the PuncAra model inserts punctuation after nearly every word in the sentence. By limiting to 3 patches, only the most confident punctuation suggestions are shown to the user.
|
| 144 |
+
|
| 145 |
+
## 6.6 Summarization Results
|
| 146 |
+
|
| 147 |
+
### 6.6.1 Decoding Strategy Comparison
|
| 148 |
+
|
| 149 |
+
| Strategy | Quality | Hallucination Risk | Selected |
|
| 150 |
+
|---|---|---|---|
|
| 151 |
+
| Greedy (num_beams=1) | Faithful, specific | Low | ✅ |
|
| 152 |
+
| Beam Search (num_beams=4) | More generic | High | ❌ |
|
| 153 |
+
| Sampling (temperature=0.7) | Creative but unreliable | Very High | ❌ |
|
| 154 |
+
|
| 155 |
+
Greedy decoding was empirically found to produce the most faithful Arabic summaries, with the lowest hallucination rate. Beam search tended to produce generic, formulaic summaries that could apply to any Arabic text.
|
| 156 |
+
|
| 157 |
+
### 6.6.2 Extractive Fallback Rate
|
| 158 |
+
|
| 159 |
+
The extractive fallback triggers when the model's output has:
|
| 160 |
+
- Overlap ratio < 0.35 (less than 35% of summary words appear in source), OR
|
| 161 |
+
- SequenceMatcher ratio < 0.22
|
| 162 |
+
|
| 163 |
+
In practice, the fallback triggers on approximately 10–15% of inputs, primarily on very short texts (< 50 words) where the model lacks sufficient context for abstraction.
|
| 164 |
+
|
| 165 |
+
## 6.7 Competitive Analysis
|
| 166 |
+
|
| 167 |
+
### 6.7.1 Feature Comparison
|
| 168 |
+
|
| 169 |
+
A comprehensive feature gap analysis was conducted against Grammarly and QuillBot:
|
| 170 |
+
|
| 171 |
+
```mermaid
|
| 172 |
+
graph LR
|
| 173 |
+
subgraph "Bayan Features"
|
| 174 |
+
B1["Spelling ✅"]
|
| 175 |
+
B2["Grammar ✅"]
|
| 176 |
+
B3["Punctuation ✅"]
|
| 177 |
+
B4["Summarization ✅"]
|
| 178 |
+
B5["Dialect Conv. ✅"]
|
| 179 |
+
B6["Autocomplete ✅"]
|
| 180 |
+
B7["Quran Verify ✅"]
|
| 181 |
+
B8["Browser Ext. ✅"]
|
| 182 |
+
B9["Side Panel ✅"]
|
| 183 |
+
B10["Inline Analysis ✅"]
|
| 184 |
+
end
|
| 185 |
+
|
| 186 |
+
subgraph "Grammarly Exclusive"
|
| 187 |
+
G1["Tone Detection ❌"]
|
| 188 |
+
G2["Plagiarism ❌"]
|
| 189 |
+
G3["Clarity Score ❌"]
|
| 190 |
+
G4["Style Guide ❌"]
|
| 191 |
+
G5["Engagement ❌"]
|
| 192 |
+
G6["Error Explanations ❌"]
|
| 193 |
+
G7["Personal Dictionary ❌"]
|
| 194 |
+
G8["Word Count Goals ❌"]
|
| 195 |
+
end
|
| 196 |
+
|
| 197 |
+
subgraph "QuillBot Exclusive"
|
| 198 |
+
Q1["Paraphraser ❌"]
|
| 199 |
+
Q2["Citation Gen. ❌"]
|
| 200 |
+
Q3["Co-Writer ❌"]
|
| 201 |
+
end
|
| 202 |
+
```
|
| 203 |
+
|
| 204 |
+
### 6.7.2 Gap Analysis Results
|
| 205 |
+
|
| 206 |
+
| Category | Total Features | Bayan Has | Gap |
|
| 207 |
+
|---|---|---|---|
|
| 208 |
+
| Core Writing | 8 | 6 | 2 (explanations, personal dict) |
|
| 209 |
+
| Analysis & Scoring | 6 | 1 | 5 (tone, clarity, engagement, etc.) |
|
| 210 |
+
| Rewriting | 5 | 1 | 4 (paraphrasing modes) |
|
| 211 |
+
| Browser Integration | 8 | 6 | 2 (keyboard, multiple browsers) |
|
| 212 |
+
| Productivity | 6 | 2 | 4 (goals, statistics, etc.) |
|
| 213 |
+
| Enterprise | 5 | 0 | 5 (admin, SSO, compliance) |
|
| 214 |
+
| Arabic-Specific | 9 | 7 | 2 (diacritization, morphological) |
|
| 215 |
+
| **Total** | **47** | **22** | **25** |
|
| 216 |
+
|
| 217 |
+
### 6.7.3 Bayan's Unique Advantages
|
| 218 |
+
|
| 219 |
+
Despite the feature count gap, Bayan offers capabilities that neither Grammarly nor QuillBot provide:
|
| 220 |
+
|
| 221 |
+
1. **Arabic language support**: The fundamental differentiator — neither competitor supports Arabic in any meaningful capacity.
|
| 222 |
+
2. **Dialect-to-MSA conversion**: No competitor offers conversion from dialectal Arabic to formal MSA.
|
| 223 |
+
3. **Quranic text verification**: A unique feature tailored to Arabic-language writing.
|
| 224 |
+
4. **Side Panel API**: Bayan leverages Chrome's Side Panel API (Chrome ≥ 114) for persistent analysis alongside browsing, a feature not available in Grammarly's extension.
|
| 225 |
+
5. **Open-source/academic**: The system is fully inspectable and modifiable, unlike closed commercial products.
|
| 226 |
+
|
| 227 |
+
## 6.8 Architecture Discussion
|
| 228 |
+
|
| 229 |
+
### 6.8.1 Sequential vs. Parallel Pipeline
|
| 230 |
+
|
| 231 |
+
The `/api/analyze` pipeline processes stages sequentially (Spelling → Grammar → Punctuation) rather than in parallel. This design was chosen because:
|
| 232 |
+
|
| 233 |
+
1. **Data dependency**: Each stage operates on the output of the previous stage. Grammar correction benefits from having spelling errors already fixed.
|
| 234 |
+
2. **Coordinate mapping**: The OffsetMapper chain requires sequential text mutations to maintain accurate coordinate transforms.
|
| 235 |
+
3. **StageLocker**: Cross-stage conflict resolution requires knowing which ranges were modified by earlier stages.
|
| 236 |
+
|
| 237 |
+
**Trade-off**: Sequential processing increases total latency (sum of stage latencies rather than max). For a typical short text, this means ~5–15 seconds total rather than the ~5 seconds that parallel execution would achieve.
|
| 238 |
+
|
| 239 |
+
### 6.8.2 Lazy Loading vs. Eager Loading
|
| 240 |
+
|
| 241 |
+
All NLP models (except summarization) use lazy loading — they are loaded on first request rather than at server startup. This design was chosen because:
|
| 242 |
+
|
| 243 |
+
1. **Cold start time**: Loading all models at startup would take 60+ seconds, causing the health check to fail and HuggingFace Spaces to mark the deployment as unhealthy.
|
| 244 |
+
2. **RAM efficiency**: Not all models may be needed for every session. Lazy loading defers the RAM allocation.
|
| 245 |
+
3. **Graceful degradation**: If a model fails to load, only that specific capability is affected.
|
| 246 |
+
|
| 247 |
+
**Trade-off**: The first request that triggers model loading experiences significantly higher latency (10–30 seconds for model initialization). Subsequent requests use the cached singleton.
|
| 248 |
+
|
| 249 |
+
### 6.8.3 Network Proxy Pattern
|
| 250 |
+
|
| 251 |
+
The Chrome extension's content script cannot make cross-origin requests to the Bayan API due to Content Security Policy restrictions. The background service worker acts as a network proxy:
|
| 252 |
+
|
| 253 |
+
```
|
| 254 |
+
Content Script → chrome.runtime.sendMessage() → Service Worker → fetch() → API
|
| 255 |
+
```
|
| 256 |
+
|
| 257 |
+
**Trade-off**: This adds one message-passing round-trip (~5ms) to every API call, which is negligible compared to the model inference time (~5–15 seconds).
|
| 258 |
+
|
| 259 |
+
### 6.8.4 Single Worker Deployment
|
| 260 |
+
|
| 261 |
+
The production deployment uses a single Gunicorn worker:
|
| 262 |
+
|
| 263 |
+
```
|
| 264 |
+
gunicorn --workers 1
|
| 265 |
+
```
|
| 266 |
+
|
| 267 |
+
This is necessary because:
|
| 268 |
+
1. Each worker loads its own copy of all models, consuming ~4.5GB RAM.
|
| 269 |
+
2. The free-tier deployment has 16GB RAM total.
|
| 270 |
+
3. A second worker would consume ~9GB for models alone, leaving insufficient RAM for the OS, Python, and request processing.
|
| 271 |
+
|
| 272 |
+
**Trade-off**: With a single worker, the server can handle only one request at a time. Concurrent requests are queued by Gunicorn. This is acceptable for the current user base but would require scaling to multiple replicas or a paid tier for production traffic.
|
| 273 |
+
|
| 274 |
+
## 6.9 Engineering Lessons Learned
|
| 275 |
+
|
| 276 |
+
### 6.9.1 The Over-Correction Problem
|
| 277 |
+
|
| 278 |
+
The most significant lesson from the AraSpell development was that **neural spelling correction models are too aggressive by default**. Without the multi-layered guard system, the model changes approximately 40% of valid Arabic words to other valid words, producing grammatically correct but semantically incorrect text. The guard system (7 guards, 200+ lines of filtering code) was developed iteratively through 37 bug reports (BUG-001 through BUG-037) during testing.
|
| 279 |
+
|
| 280 |
+
### 6.9.2 Coordinate Mapping Complexity
|
| 281 |
+
|
| 282 |
+
Maintaining accurate character offsets through a multi-stage text mutation pipeline is inherently complex. The OffsetMapper + PipelineContext architecture (248 lines) was developed after two failed approaches:
|
| 283 |
+
1. **Attempt 1**: Simple offset arithmetic (failed on multi-word replacements)
|
| 284 |
+
2. **Attempt 2**: Character-level diff tracking (too slow for long texts)
|
| 285 |
+
3. **Final**: `difflib.SequenceMatcher`-based mapping with monotonicity guards
|
| 286 |
+
|
| 287 |
+
### 6.9.3 Simplicity as Architecture
|
| 288 |
+
|
| 289 |
+
The Phase 7.1 stabilization sprint demonstrated that **removing code can improve system quality more than adding code**. The sprint removed 458 lines while maintaining 100% test pass rate, by consolidating duplicated retry, cache, hash, API URL, and versioning systems.
|
| 290 |
+
|
| 291 |
+
### 6.9.4 Graceful Degradation over Hard Failure
|
| 292 |
+
|
| 293 |
+
Every failure point in the system returns a degraded but valid response rather than an error:
|
| 294 |
+
- Spelling failure → Grammar + Punctuation still run
|
| 295 |
+
- Grammar failure → Spelling + Punctuation still run
|
| 296 |
+
- Model load failure → Endpoint returns 503 with clear error message
|
| 297 |
+
- Autocomplete failure → Returns empty suggestions array
|
| 298 |
+
- Network failure in extension → Error recovery mode with backoff
|
| 299 |
+
|
| 300 |
+
## 6.10 Limitations and Constraints
|
| 301 |
+
|
| 302 |
+
### 6.10.1 Performance Constraints
|
| 303 |
+
|
| 304 |
+
| Constraint | Impact | Mitigation |
|
| 305 |
+
|---|---|---|
|
| 306 |
+
| Single Gunicorn worker | No concurrent requests | Acceptable for current scale |
|
| 307 |
+
| CPU-only inference | Slower than GPU | Required by free tier |
|
| 308 |
+
| AraSpell skip > 300 chars | No spelling for long texts | Grammar catches most orthographic errors |
|
| 309 |
+
| Gradio round-trip | Grammar latency 2–8s | Retry with backoff |
|
| 310 |
+
|
| 311 |
+
### 6.10.2 Accuracy Constraints
|
| 312 |
+
|
| 313 |
+
| Constraint | Impact | Mitigation |
|
| 314 |
+
|---|---|---|
|
| 315 |
+
| CAMeL MLE ~90% POS accuracy | Some grammar rules misfire | Known plurals whitelist |
|
| 316 |
+
| AraSpell false negatives | Some misspellings missed | Grammar model as backup |
|
| 317 |
+
| PuncAra over-punctuation | Excessive commas/periods | 3-patch aggregate cap |
|
| 318 |
+
| Hallucination risk | Models may generate nonsense | Multi-layered validation |
|
| 319 |
+
|
| 320 |
+
### 6.10.3 Platform Constraints
|
| 321 |
+
|
| 322 |
+
| Constraint | Impact | Mitigation |
|
| 323 |
+
|---|---|---|
|
| 324 |
+
| Chrome-only extension | No Firefox/Safari support | Web app as fallback |
|
| 325 |
+
| Protected pages | No analysis on chrome:// | Graceful skip |
|
| 326 |
+
| Shadow DOM | Cannot access React/Angular internals | Detection heuristics |
|
| 327 |
+
| No offline mode | Requires network for API | Error recovery mode |
|
| 328 |
+
|
| 329 |
+
## 6.11 Summary
|
| 330 |
+
|
| 331 |
+
The Bayan system successfully delivers the first comprehensive Arabic writing assistant that integrates seven NLP capabilities (spelling, grammar, punctuation, summarization, dialect conversion, autocomplete, Quranic verification) within a unified platform. The system is deployed in production on HuggingFace Spaces, accessible via a web application and a Chrome Manifest V3 extension with Grammarly-style inline analysis.
|
| 332 |
+
|
| 333 |
+
Key achievements include:
|
| 334 |
+
- **Multi-stage spelling pipeline** with 7 guard layers preventing ~55% of model false positives
|
| 335 |
+
- **Hybrid grammar system** combining neural inference with rule-based post-processing
|
| 336 |
+
- **Production-hardened pipeline** with deterministic overlap resolution, coordinate mapping, and cross-stage conflict prevention
|
| 337 |
+
- **458 lines of code removed** during stabilization while maintaining 100% test pass rate
|
| 338 |
+
- **Graceful degradation** at every failure point
|
| 339 |
+
|
| 340 |
+
The system demonstrates that Arabic NLP has matured to the point where a comprehensive writing assistant is technically feasible, though significant work remains to match the depth and breadth of English-language tools like Grammarly.
|
docs/Chapter_7_Conclusion_and_Future_Work.md
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Chapter 7: Conclusion and Future Work
|
| 2 |
+
|
| 3 |
+
## 7.1 Summary of Contributions
|
| 4 |
+
|
| 5 |
+
This project has designed, implemented, and deployed **Bayan** (بيان) — the first comprehensive, AI-powered Arabic writing assistant that integrates seven core NLP capabilities within a unified, production-ready platform. The system is accessible through a full-featured web application and a Chrome Manifest V3 browser extension with Grammarly-style inline analysis.
|
| 6 |
+
|
| 7 |
+
The principal contributions of this work are:
|
| 8 |
+
|
| 9 |
+
### 7.1.1 Arabic NLP Pipeline
|
| 10 |
+
|
| 11 |
+
1. **AraSpell Spelling Correction Pipeline**: A novel 9-stage spelling correction pipeline for Arabic, combining rule-based preprocessing, neural correction (AraBERT Encoder-Decoder), hybrid word alignment, contextual refinement (BERT MLM), and vocabulary-aware post-processing. The system includes 7 guard layers that prevent approximately 55% of the model's raw proposals from reaching the user, eliminating meaning-changing false positives without sacrificing true positive detection. The guard system addresses previously undocumented Arabic NLP challenges including in-vocabulary-to-in-vocabulary corruption, pronoun suffix false positives, and numeral hallucination. Total implementation: 1,507 lines of Python.
|
| 12 |
+
|
| 13 |
+
2. **PuncAra-v1 Punctuation Restoration Model**: A custom-trained EncoderDecoderModel for Arabic punctuation restoration, featuring windowed chunking for long texts and a non-punctuation change stripping layer (Fix P1) that ensures the model's output contains only punctuation modifications.
|
| 14 |
+
|
| 15 |
+
3. **Hybrid Grammar Correction**: A two-tier grammar correction system combining neural inference (Gemma 3 via Gradio) with rule-based post-processing (8 grammar rule categories implemented using CAMeL Tools morphological analysis), covering number-gender agreement, case marking, verb conjugation, demonstrative agreement, five nouns declension, and subject-verb agreement.
|
| 16 |
+
|
| 17 |
+
4. **Production Analysis Pipeline**: A three-stage sequential pipeline (Spelling → Grammar → Punctuation) with coordinate mapping (`OffsetMapper`), cross-stage conflict resolution (`StageLocker`), deterministic overlap resolution (`PatchSet`), and dual coordinate spaces (`CorrectionPatch`), enabling accurate suggestion delivery despite multi-stage text mutation.
|
| 18 |
+
|
| 19 |
+
### 7.1.2 Platform Engineering
|
| 20 |
+
|
| 21 |
+
5. **Chrome Manifest V3 Extension**: A production-grade Chrome browser extension implementing:
|
| 22 |
+
- Grammarly-style inline error highlighting on arbitrary web pages
|
| 23 |
+
- Persistent side panel via Chrome's Side Panel API
|
| 24 |
+
- Popup interface for quick text analysis
|
| 25 |
+
- Context menu integration for right-click analysis
|
| 26 |
+
- Protected site detection and error recovery mode
|
| 27 |
+
|
| 28 |
+
6. **Full-Stack Web Application**: A single-page web application with a WYSIWYG Arabic text editor, real-time analysis, document management (local + Supabase cloud sync), user authentication, theme support, autocomplete, and summarization.
|
| 29 |
+
|
| 30 |
+
7. **Docker Deployment**: A containerized deployment on HuggingFace Spaces with pre-cached models, graceful degradation, health monitoring, and single-worker memory optimization for free-tier infrastructure.
|
| 31 |
+
|
| 32 |
+
### 7.1.3 Additional NLP Capabilities
|
| 33 |
+
|
| 34 |
+
8. **Dialect-to-MSA Conversion**: An mT5-based model converting Egyptian, Gulf, Levantine, and Maghrebi dialects to Modern Standard Arabic.
|
| 35 |
+
|
| 36 |
+
9. **Hybrid Autocomplete**: A bigram + AraGPT2 hybrid system providing context-aware next-word prediction with configurable statistical-neural weighting.
|
| 37 |
+
|
| 38 |
+
10. **Quranic Text Verification**: A SQLite-backed fuzzy search engine for identifying and cross-referencing Quranic quotations.
|
| 39 |
+
|
| 40 |
+
## 7.2 Objectives Achievement
|
| 41 |
+
|
| 42 |
+
| Objective | Status | Notes |
|
| 43 |
+
|---|---|---|
|
| 44 |
+
| Custom Arabic spelling model (AraSpell) | ✅ Achieved | AraBERT Enc-Dec + 9-stage pipeline |
|
| 45 |
+
| Custom Arabic punctuation model (PuncAra-v1) | ✅ Achieved | EncoderDecoderModel + Fix P1 |
|
| 46 |
+
| Arabic grammar correction | ✅ Achieved | Gemma 3 + 8 CAMeL rules |
|
| 47 |
+
| Arabic text summarization | ✅ Achieved | mBART + extractive fallback |
|
| 48 |
+
| Dialect-to-MSA conversion | ✅ Achieved | mT5 with task prefix |
|
| 49 |
+
| Hybrid autocomplete | ✅ Achieved | Bigram + AraGPT2 |
|
| 50 |
+
| Quranic text verification | ✅ Achieved | SQLite fuzzy search |
|
| 51 |
+
| Full-stack web application | ✅ Achieved | Flask + SPA + WYSIWYG editor |
|
| 52 |
+
| Chrome browser extension | ✅ Achieved | MV3 + inline + side panel + popup |
|
| 53 |
+
| Production deployment | ✅ Achieved | Docker on HuggingFace Spaces |
|
| 54 |
+
|
| 55 |
+
All 10 project objectives were fully achieved.
|
| 56 |
+
|
| 57 |
+
## 7.3 Key Findings
|
| 58 |
+
|
| 59 |
+
### 7.3.1 Arabic NLP Maturity
|
| 60 |
+
|
| 61 |
+
The project demonstrates that pre-trained Arabic language models (AraBERT, AraGPT2, mBART, mT5, Gemma 3) have reached sufficient maturity to support a comprehensive writing assistant, provided that extensive post-processing and guard systems are implemented. The raw model outputs are not adequate for production use — the engineering effort in filtering, validation, and cross-stage coordination exceeds the effort in model training and inference.
|
| 62 |
+
|
| 63 |
+
### 7.3.2 The Guard System Paradigm
|
| 64 |
+
|
| 65 |
+
The most impactful contribution of this work may be the guard system paradigm for Arabic spelling correction. The finding that ~55% of a well-trained neural model's proposals must be filtered before user presentation challenges the prevailing assumption that larger or better-trained models inherently produce production-ready output. The guard system taxonomy (numeral protection, directional blocks, IV→IV guard, pronoun suffix guard, Levenshtein filter, orthographic filter, confidence dampening) provides a reusable framework for other Arabic NLP systems.
|
| 66 |
+
|
| 67 |
+
### 7.3.3 Production Hardening Impact
|
| 68 |
+
|
| 69 |
+
The Phase 7.1 stabilization sprint demonstrated that removing 458 lines of duplicated infrastructure while maintaining 100% test pass rate is not only possible but beneficial. The resulting system is simpler, more maintainable, and more predictable than its predecessor.
|
| 70 |
+
|
| 71 |
+
## 7.4 Limitations
|
| 72 |
+
|
| 73 |
+
### 7.4.1 Technical Limitations
|
| 74 |
+
|
| 75 |
+
1. **Single-threaded serving**: The single Gunicorn worker limits concurrent request handling. Under load, requests queue sequentially.
|
| 76 |
+
|
| 77 |
+
2. **AraSpell performance ceiling**: The 300-character threshold for enabling spelling correction is a pragmatic compromise. Improving AraSpell's inference speed (currently ~50 seconds for long texts) would allow full-pipeline analysis on longer documents.
|
| 78 |
+
|
| 79 |
+
3. **Grammar model dependency**: The Gemma 3 grammar model is accessed via a Gradio-hosted endpoint, introducing a network dependency, additional latency, and a single point of failure that cannot be resolved without hosting the model locally.
|
| 80 |
+
|
| 81 |
+
4. **No offline capability**: All NLP features require network access to the Bayan API, meaning the extension cannot function offline.
|
| 82 |
+
|
| 83 |
+
### 7.4.2 Scope Limitations
|
| 84 |
+
|
| 85 |
+
1. **No diacritization**: The system processes unvoweled Arabic text and does not generate diacritical marks.
|
| 86 |
+
|
| 87 |
+
2. **No error explanations**: Unlike Grammarly, Bayan does not explain why a correction was suggested. Users must evaluate corrections based on the corrected text alone.
|
| 88 |
+
|
| 89 |
+
3. **No personal dictionary**: Users cannot add custom words to prevent false positives on domain-specific vocabulary.
|
| 90 |
+
|
| 91 |
+
4. **No paraphrasing**: Unlike QuillBot, Bayan does not offer text rewriting or paraphrasing capabilities.
|
| 92 |
+
|
| 93 |
+
5. **Chrome-only**: The browser extension is limited to Chromium-based browsers. Firefox and Safari are not supported.
|
| 94 |
+
|
| 95 |
+
## 7.5 Future Work
|
| 96 |
+
|
| 97 |
+
Based on the competitive gap analysis and technical assessment, the following roadmap is proposed:
|
| 98 |
+
|
| 99 |
+
### 7.5.1 Phase 8: Error Explanations (High Priority)
|
| 100 |
+
|
| 101 |
+
**Objective**: Provide users with educational explanations for each correction.
|
| 102 |
+
|
| 103 |
+
**Approach**: Attach a description template to each correction type:
|
| 104 |
+
- Spelling: "الكلمة 'X' هي الشكل الصحيح إملائيًا للكلمة 'Y'"
|
| 105 |
+
- Grammar (preposition): "بعد حرف الجر 'في'، يجب أن تكون الكلمة مجرورة"
|
| 106 |
+
- Punctuation: "يُنصح بوضع فاصلة هنا لتحسين وضوح الجملة"
|
| 107 |
+
|
| 108 |
+
**Estimated effort**: Medium — requires mapping each guard/rule to an explanation template.
|
| 109 |
+
|
| 110 |
+
### 7.5.2 Phase 9: Paraphrasing Engine (High Priority)
|
| 111 |
+
|
| 112 |
+
**Objective**: Allow users to rephrase text in multiple styles (formal, simple, creative).
|
| 113 |
+
|
| 114 |
+
**Approach**: Fine-tune an Arabic seq2seq model (mT5 or AraBART) on parallel paraphrase corpora.
|
| 115 |
+
|
| 116 |
+
**Estimated effort**: High — requires training data collection and model fine-tuning.
|
| 117 |
+
|
| 118 |
+
### 7.5.3 Phase 10: Diacritization (Medium Priority)
|
| 119 |
+
|
| 120 |
+
**Objective**: Generate diacritical marks (tashkīl) for Arabic text.
|
| 121 |
+
|
| 122 |
+
**Approach**: Integrate an existing Arabic diacritization model (e.g., Mishkal or Shakkala) or fine-tune a model on diacritized corpora.
|
| 123 |
+
|
| 124 |
+
**Estimated effort**: Medium — pre-trained models exist and can be integrated.
|
| 125 |
+
|
| 126 |
+
### 7.5.4 Phase 11: Personal Dictionary (Medium Priority)
|
| 127 |
+
|
| 128 |
+
**Objective**: Allow users to add custom words that should not be flagged as spelling errors.
|
| 129 |
+
|
| 130 |
+
**Approach**: Maintain a per-user word list in Supabase, consulted before the spelling guard system.
|
| 131 |
+
|
| 132 |
+
**Estimated effort**: Low — primarily a UI and storage feature.
|
| 133 |
+
|
| 134 |
+
### 7.5.5 Phase 12: Tone Detection (Lower Priority)
|
| 135 |
+
|
| 136 |
+
**Objective**: Detect and suggest adjustments to the tone of Arabic text (formal, informal, emotional, neutral).
|
| 137 |
+
|
| 138 |
+
**Approach**: Fine-tune a text classification model on Arabic text annotated with tone labels.
|
| 139 |
+
|
| 140 |
+
**Estimated effort**: High — requires annotated training data.
|
| 141 |
+
|
| 142 |
+
### 7.5.6 Phase 13: Multi-Browser Support (Lower Priority)
|
| 143 |
+
|
| 144 |
+
**Objective**: Port the Chrome extension to Firefox (using WebExtension APIs) and Safari (using Safari Web Extensions).
|
| 145 |
+
|
| 146 |
+
**Approach**: Refactor the extension to use the WebExtension API baseline, with polyfills for Chrome-specific APIs (Side Panel, etc.).
|
| 147 |
+
|
| 148 |
+
**Estimated effort**: Medium — Firefox support is straightforward; Safari requires an Xcode wrapper.
|
| 149 |
+
|
| 150 |
+
### 7.5.7 Phase 14: Performance Optimization
|
| 151 |
+
|
| 152 |
+
**Objective**: Reduce API latency and enable analysis of longer texts.
|
| 153 |
+
|
| 154 |
+
**Potential approaches**:
|
| 155 |
+
- ONNX Runtime for CPU inference optimization
|
| 156 |
+
- Model quantization (INT8) for reduced memory and faster inference
|
| 157 |
+
- Batch processing for multiple sentences
|
| 158 |
+
- Edge inference (WebAssembly/ONNX in browser) for latency-sensitive operations
|
| 159 |
+
|
| 160 |
+
### 7.5.8 Long-Term Vision
|
| 161 |
+
|
| 162 |
+
```mermaid
|
| 163 |
+
timeline
|
| 164 |
+
title Bayan Development Roadmap
|
| 165 |
+
section Near-Term
|
| 166 |
+
Phase 8 : Error Explanations
|
| 167 |
+
Phase 9 : Paraphrasing Engine
|
| 168 |
+
section Mid-Term
|
| 169 |
+
Phase 10 : Diacritization
|
| 170 |
+
Phase 11 : Personal Dictionary
|
| 171 |
+
Phase 12 : Tone Detection
|
| 172 |
+
section Long-Term
|
| 173 |
+
Phase 13 : Multi-Browser Support
|
| 174 |
+
Phase 14 : Performance Optimization
|
| 175 |
+
Phase 15 : Mobile Keyboard
|
| 176 |
+
Phase 16 : API Platform
|
| 177 |
+
```
|
| 178 |
+
|
| 179 |
+
## 7.6 Reflections
|
| 180 |
+
|
| 181 |
+
The Bayan project began as a graduation capstone with the ambitious goal of creating an Arabic Grammarly. The resulting system, while not matching Grammarly's 15+ years of development and resources, demonstrates that a small team can build a functional, production-ready Arabic writing assistant using modern NLP techniques and open-source tools.
|
| 182 |
+
|
| 183 |
+
The most important lesson from this project is that **production-ready NLP is 20% model training and 80% engineering**. The models themselves (AraBERT, Gemma 3, mBART, mT5, AraGPT2) are off-the-shelf or fine-tuned from existing architectures. The true complexity lies in the guard systems, coordinate mapping, cross-stage conflict resolution, graceful degradation, and the thousand small decisions that determine whether a user trusts the tool or abandons it after the first false positive.
|
| 184 |
+
|
| 185 |
+
Arabic deserves better writing tools. Bayan is a step toward that future.
|
| 186 |
+
|
| 187 |
+
## 7.7 Final System Statistics
|
| 188 |
+
|
| 189 |
+
| Metric | Value |
|
| 190 |
+
|---|---|
|
| 191 |
+
| Total estimated lines of code | ~16,000+ |
|
| 192 |
+
| NLP models integrated | 7 (AraSpell, Gemma 3, PuncAra, mBART, mT5, AraGPT2, SQLite) |
|
| 193 |
+
| API endpoints | 10 |
|
| 194 |
+
| Pipeline stages | 3 (Spelling → Grammar → Punctuation) |
|
| 195 |
+
| Spelling guards | 7 layers |
|
| 196 |
+
| Grammar rules | 8 categories |
|
| 197 |
+
| Unit tests | 49 (100% pass) |
|
| 198 |
+
| Chrome extension components | 4 (popup, side panel, inline, context menu) |
|
| 199 |
+
| Deployment platform | HuggingFace Spaces (Docker) |
|
| 200 |
+
| Production RAM footprint | ~4.5GB |
|
| 201 |
+
| Supported dialects | 4+ (Egyptian, Gulf, Levantine, Maghrebi) |
|
| 202 |
+
| Quran database size | ~22MB (complete Quran with translations) |
|
| 203 |
+
| Development phases | 7 (completed) |
|
| 204 |
+
| Code removed in stabilization | 458 lines |
|
| 205 |
+
| Net code reduction in Phase 7.1 | 346 lines |
|
| 206 |
+
|
| 207 |
+
---
|
| 208 |
+
|
| 209 |
+
*"بيان — لأن العربية تستحق الأفضل"*
|
| 210 |
+
|
| 211 |
+
*"Bayan — because Arabic deserves the best."*
|
exhaustive_check.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
"""
|
| 2 |
-
|
|
|
|
| 3 |
"""
|
| 4 |
import re, os, glob
|
| 5 |
|
|
@@ -15,7 +16,7 @@ ui = load('src/js/ui.js')
|
|
| 15 |
fmt = load('src/js/format.js')
|
| 16 |
docs_ui = load('src/js/documents-cloud/documents-ui.js')
|
| 17 |
renderer = load('src/js/renderer.js')
|
| 18 |
-
|
| 19 |
|
| 20 |
all_js = ''
|
| 21 |
for f in glob.glob('src/js/**/*.js', recursive=True):
|
|
@@ -42,165 +43,172 @@ def section(t):
|
|
| 42 |
section('PHASE 1: tokens.css')
|
| 43 |
# ═══════════════════════════════════════════
|
| 44 |
c('--shadow-xs', '--shadow-xs' in tokens)
|
| 45 |
-
c('--shadow-glow', '--shadow-glow' in tokens)
|
| 46 |
-
c('--transition-spring', '--transition-spring' in tokens)
|
| 47 |
c('--gradient-primary', '--gradient-primary' in tokens)
|
| 48 |
c('--gradient-surface', '--gradient-surface' in tokens)
|
| 49 |
-
c('--radius-xl: 1.5rem', '--radius-xl' in tokens)
|
| 50 |
-
c('--color-skeleton
|
| 51 |
|
| 52 |
section('PHASE 1: base.css')
|
| 53 |
-
c('Custom scrollbar
|
| 54 |
-
c('Custom scrollbar
|
|
|
|
| 55 |
c('scroll-behavior: smooth', 'scroll-behavior: smooth' in base)
|
| 56 |
c('::selection highlight', '::selection' in base)
|
| 57 |
-
c('Focus-visible outlines', 'focus-visible' in base
|
| 58 |
-
c('
|
| 59 |
-
c('
|
| 60 |
c('.skeleton class', '.skeleton' in base)
|
| 61 |
c('Button press scale(0.97)', 'scale(0.97)' in base)
|
| 62 |
|
| 63 |
section('PHASE 1: components.css — Navigation')
|
| 64 |
-
c('Glassmorphism blur(16px) saturate(180%)', 'saturate(180%)' in comp)
|
| 65 |
-
c('Bottom border glow on scroll
|
| 66 |
-
c('Active nav link
|
| 67 |
|
| 68 |
section('PHASE 1: components.css — Buttons')
|
| 69 |
c('Gradient-accent transition', 'transition' in comp)
|
| 70 |
-
c('Press state scale(0.97)', 'scale(0.97)' in base
|
| 71 |
-
c('Disabled
|
| 72 |
c('Focus-visible ring', 'focus-visible' in comp)
|
| 73 |
-
c('Feedback state (
|
| 74 |
|
| 75 |
section('PHASE 1: components.css — Cards')
|
| 76 |
-
c('Card hover
|
| 77 |
-
c('Feature icon pulse on hover', 'pulse' in comp.lower()
|
| 78 |
|
| 79 |
section('PHASE 1: components.css — Modals')
|
| 80 |
-
c('Modal
|
| 81 |
-
c('Modal exit slide-down + fade', 'slideDown' in comp or 'fadeOut' in comp or 'modalSlideUp' in comp)
|
| 82 |
|
| 83 |
section('PHASE 1: components.css — Toast')
|
| 84 |
-
c('Toast
|
| 85 |
-
c('Toast
|
|
|
|
|
|
|
| 86 |
|
| 87 |
section('PHASE 1: components.css — Skeleton')
|
| 88 |
c('.skeleton shimmer animation', '.skeleton' in base and 'shimmer' in base)
|
| 89 |
-
c('.skeleton-text variant', '.skeleton-text' in base or '.skeleton' in base)
|
| 90 |
|
| 91 |
section('PHASE 1: components.css — Empty States')
|
| 92 |
-
c('.empty-state component', '.empty-state' in comp)
|
| 93 |
-
c('Applied to empty doc list', 'empty-state' in docs_ui)
|
| 94 |
-
c('Applied to empty suggestions', '\u0644\u0627 \u062a\u0648\u062c\u062f' in ui or 'empty' in ui.lower())
|
| 95 |
|
| 96 |
section('PHASE 1: components.css — Confirm Dialog')
|
| 97 |
-
c('.confirm-dialog
|
| 98 |
-
c('showConfirmDialog()
|
| 99 |
|
| 100 |
section('PHASE 1: components.css — Bottom Sheet')
|
| 101 |
-
c('Bottom sheet
|
| 102 |
-
c('
|
| 103 |
|
| 104 |
section('PHASE 1: components.css — Pricing')
|
| 105 |
c('.pricing-glow active plan', '.pricing-glow' in comp)
|
| 106 |
-
c('
|
| 107 |
-
c('
|
| 108 |
|
| 109 |
# ═══════════════════════════════════════════
|
| 110 |
section('PHASE 2: Brand Identity')
|
| 111 |
# ═══════════════════════════════════════════
|
| 112 |
c('Favicon SVG exists', os.path.exists('src/favicon.svg'))
|
| 113 |
-
c('Nav logo SVG (
|
| 114 |
-
c('Footer logo consistent', html.count('grad1') >= 2)
|
| 115 |
-
c('Wordmark text-gradient
|
| 116 |
-
c('
|
| 117 |
|
| 118 |
# ═══════════════════════════════════════════
|
| 119 |
section('PHASE 3: Landing Page — Hero')
|
| 120 |
# ═══════════════════════════════════════════
|
| 121 |
-
c('Subheadline
|
| 122 |
-
c('Subheadline
|
| 123 |
-
c('
|
| 124 |
-
c('
|
|
|
|
|
|
|
| 125 |
|
| 126 |
section('PHASE 3: Landing Page — Features Preview')
|
| 127 |
c('Feature cards present', 'feature' in html.lower())
|
| 128 |
-
c('
|
|
|
|
| 129 |
|
| 130 |
section('PHASE 3: Landing Page — How It Works')
|
| 131 |
-
c('Step numbers
|
| 132 |
-
c('
|
| 133 |
|
| 134 |
# ═══════════════════════════════════════════
|
| 135 |
section('PHASE 4: Features Page')
|
| 136 |
# ═══════════════════════════════════════════
|
| 137 |
-
c('page-features exists', 'page-features' in html)
|
| 138 |
-
c('Bayyinah CTA with
|
|
|
|
| 139 |
|
| 140 |
# ═══════════════════════════════════════════
|
| 141 |
section('PHASE 5: Pricing Page')
|
| 142 |
# ═══════════════════════════════════════════
|
| 143 |
c('pricing-glow in HTML', 'pricing-glow' in html)
|
| 144 |
c('beta-shimmer in HTML', 'beta-shimmer' in html)
|
| 145 |
-
c('
|
| 146 |
-
c('
|
| 147 |
-
c('Pricing CTA arrow', '\u2190' in html)
|
|
|
|
| 148 |
|
| 149 |
# ═══════════════════════════════════════════
|
| 150 |
section('PHASE 6.1: Editor Toolbar')
|
| 151 |
# ═══════════════════════════════════════════
|
| 152 |
-
c('macOS dots
|
| 153 |
-
c('Red dot tooltip
|
| 154 |
-
c('Yellow dot tooltip
|
| 155 |
-
c('Green dot tooltip
|
| 156 |
-
c('Red dot
|
| 157 |
-
c('
|
| 158 |
|
| 159 |
section('PHASE 6.2: Format Toolbar')
|
| 160 |
-
c('Bold tooltip
|
| 161 |
-
c('Italic tooltip
|
| 162 |
-
c('Underline tooltip
|
| 163 |
-
c('Undo tooltip
|
| 164 |
-
c('Redo tooltip
|
| 165 |
-
c('
|
| 166 |
-
c('Active item highlight', '
|
| 167 |
-
c('Keyboard nav
|
| 168 |
-
c('Close on click outside',
|
| 169 |
-
c('Close on Escape', "Escape" in fmt)
|
| 170 |
c('COLOR_PALETTE swatches', 'COLOR_PALETTE' in fmt)
|
| 171 |
-
c('Color reset to default
|
| 172 |
|
| 173 |
section('PHASE 6.3: Editor Surface')
|
| 174 |
-
c('Placeholder
|
| 175 |
|
| 176 |
section('PHASE 6.4: Suggestion Popover')
|
| 177 |
-
c('
|
| 178 |
-
c('
|
| 179 |
-
c('
|
|
|
|
| 180 |
|
| 181 |
section('PHASE 6.5: Suggestion Sidebar')
|
| 182 |
-
c('Empty: \u0646\u0635\u0643
|
| 183 |
-
c('
|
| 184 |
-
c('Score ring', 'score-circle' in html)
|
| 185 |
-
c('Shimmer
|
| 186 |
|
| 187 |
section('PHASE 6.6: Editor Footer Stats')
|
| 188 |
-
c('char-count
|
| 189 |
-
c('sentence-count
|
| 190 |
-
c('reading-time
|
| 191 |
c('Word goal', 'word-goal' in html or 'wordGoal' in all_js)
|
| 192 |
-
c('Save toast', '\u062a\u0645 \u0627\u0644\u062d\u0641\u0638' in all_js or '\
|
| 193 |
-
c('Copy toast', '\u062a\u0645 \u0627\u0644\u0646\u0633\u062e' in
|
| 194 |
-
c('
|
|
|
|
| 195 |
|
| 196 |
section('PHASE 6.7: Documents Panel')
|
| 197 |
-
c('Docs empty state icon', 'empty-
|
| 198 |
-
c('Docs
|
| 199 |
-
c('Docs
|
| 200 |
|
| 201 |
section('PHASE 6.8: Summarize Panel')
|
| 202 |
-
c('Summary loading
|
| 203 |
-
c('Summary error
|
| 204 |
c('Summary mode toggle', 'summary-mode' in html)
|
| 205 |
c('Summary stats', 'summary-stats' in html)
|
| 206 |
c('Summary copy button', 'copySummary' in html)
|
|
@@ -208,96 +216,128 @@ c('Summary export dropdown', 'exportSummaryAs' in html)
|
|
| 208 |
|
| 209 |
section('PHASE 6.9: Dialect Panel')
|
| 210 |
c('Dialect char counter', 'dialect-char-count' in html)
|
| 211 |
-
c('Dialect loading
|
| 212 |
-
c('Dialect error
|
| 213 |
-
c('Dialect
|
| 214 |
-
c('Dialect copy
|
| 215 |
|
| 216 |
section('PHASE 6.10: Quran Modal')
|
| 217 |
-
|
|
|
|
|
|
|
|
|
|
| 218 |
c('Ctrl+Q shortcut (KeyQ)', 'KeyQ' in html)
|
|
|
|
| 219 |
c('Copy verified text', '\u062a\u0645 \u0646\u0633\u062e \u0627\u0644\u0646\u0635 \u0627\u0644\u0645\u062f\u0642\u0642' in html)
|
| 220 |
c('Apply verified text', '\u062a\u0645 \u062a\u0637\u0628\u064a\u0642 \u0627\u0644\u0646\u0635 \u0627\u0644\u0642\u0631\u0622\u0646\u064a' in html)
|
| 221 |
-
c('
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
|
| 223 |
section('PHASE 6.11: Mobile Components')
|
| 224 |
-
c('Bottom sheet suggestions', 'bottom-sheet' in html)
|
| 225 |
c('Mobile drawer', 'mobile-drawer' in html)
|
| 226 |
c('Mobile menu button', 'mobile-menu-btn' in html)
|
|
|
|
| 227 |
|
| 228 |
# ═══════════════════════════════════════════
|
| 229 |
section('PHASE 7.1: Auth Flows')
|
| 230 |
# ═══════════════════════════════════════════
|
| 231 |
c('Auth gate modal', 'auth-gate' in html)
|
| 232 |
-
c('Google sign-in', 'google' in all_js.lower())
|
| 233 |
c('Guest flow', '\u0627\u0644\u062a\u062c\u0631\u0628\u0629' in html or 'guest' in all_js.lower())
|
| 234 |
c('Offline banner', 'offline-banner' in html)
|
| 235 |
|
| 236 |
section('PHASE 7.2: Document Flows')
|
| 237 |
-
c('Doc save toast', '\u062a\u0645 \u0627\u0644\u062d\u0641\u0638' in all_js or '\
|
| 238 |
-
c('Doc delete custom dialog', 'showConfirmDialog' in docs_ui)
|
| 239 |
|
| 240 |
section('PHASE 7.3: Summary Flow')
|
| 241 |
-
c('Summary loading state', '
|
| 242 |
|
| 243 |
-
section('PHASE 7.4: Settings')
|
| 244 |
-
c('
|
|
|
|
| 245 |
|
| 246 |
section('PHASE 7.5: Refresh/Restore')
|
| 247 |
-
c('
|
| 248 |
-
c('
|
| 249 |
|
| 250 |
section('PHASE 7.6: Error States')
|
| 251 |
-
c('/api/analyze error toast', 'showToast' in editor)
|
| 252 |
-
c('/api/
|
| 253 |
-
c('/api/
|
| 254 |
-
c('
|
| 255 |
-
c('
|
|
|
|
| 256 |
|
| 257 |
section('PHASE 7.7: Empty States')
|
| 258 |
-
c('Editor placeholder', '
|
| 259 |
c('Documents empty state', 'empty-state' in docs_ui)
|
| 260 |
-
c('Suggestions
|
|
|
|
|
|
|
| 261 |
|
| 262 |
# ═══════════════════════════════════════════
|
| 263 |
section('PHASE 8: Responsive Design')
|
| 264 |
# ═══════════════════════════════════════════
|
| 265 |
-
c('
|
| 266 |
c('Mobile breakpoint 768px', '768px' in all_css or '768px' in html)
|
| 267 |
-
c('Bottom sheet mobile', 'bottom-sheet' in html)
|
| 268 |
-
c('Mobile drawer', 'mobile-drawer' in html)
|
|
|
|
|
|
|
| 269 |
|
| 270 |
# ═══════════════════════════════════════════
|
| 271 |
section('PHASE 9: Global Polish')
|
| 272 |
# ═══════════════════════════════════════════
|
| 273 |
-
|
| 274 |
-
c('Meta desc:
|
| 275 |
-
c('Meta desc:
|
| 276 |
-
c('404 arrow:
|
| 277 |
c('Scroll-to-top button', 'scroll-top-btn' in html)
|
| 278 |
-
c('Toast
|
| 279 |
-
c('Toast
|
|
|
|
|
|
|
| 280 |
|
| 281 |
# ═══════════════════════════════════════════
|
| 282 |
section('ARCHITECTURAL SAFETY')
|
| 283 |
# ═══════════════════════════════════════════
|
| 284 |
-
c('renderer.js preserved', 'render' in renderer)
|
| 285 |
-
c('selection.js
|
| 286 |
-
c('selection.js
|
| 287 |
-
c('No React
|
|
|
|
|
|
|
| 288 |
c('Core: getEditorText()', 'getEditorText' in editor)
|
| 289 |
c('Core: /api/analyze', '/api/analyze' in editor)
|
|
|
|
|
|
|
| 290 |
c('Core: restoreSelection()', 'restoreSelection' in editor)
|
| 291 |
|
| 292 |
# ═══════════════════════════════════════════
|
| 293 |
section('6 DESIGN DECISIONS')
|
| 294 |
# ═══════════════════════════════════════════
|
| 295 |
-
c('D1:
|
| 296 |
-
c('D2: Quran+Dialect in Pricing', '\u062a\u062f\u0642\u064a\u0642 \u0627\u0644\u0646\u0635 \u0627\u0644\u0642\u0631\u0622\u0646\u064a' in html)
|
| 297 |
-
c('D3:
|
| 298 |
c('D4: Shimmer skeletons', '@keyframes shimmer' in base)
|
| 299 |
-
c('D5: macOS dots + tooltips', '\u0645\u0633\u062d \u0627\u0644\u0645\u062d\u0631\u0631' in html)
|
| 300 |
-
c('D6: Auto-sync (no settings)', 'localStorage' in all_js)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
|
| 302 |
# ═══════════════════════════════════════════
|
| 303 |
# PRINT RESULTS
|
|
|
|
| 1 |
"""
|
| 2 |
+
ULTRA-EXHAUSTIVE: Every single bullet point from the plan.
|
| 3 |
+
Fixes false negatives from v1.
|
| 4 |
"""
|
| 5 |
import re, os, glob
|
| 6 |
|
|
|
|
| 16 |
fmt = load('src/js/format.js')
|
| 17 |
docs_ui = load('src/js/documents-cloud/documents-ui.js')
|
| 18 |
renderer = load('src/js/renderer.js')
|
| 19 |
+
selection_js = load('src/js/selection.js')
|
| 20 |
|
| 21 |
all_js = ''
|
| 22 |
for f in glob.glob('src/js/**/*.js', recursive=True):
|
|
|
|
| 43 |
section('PHASE 1: tokens.css')
|
| 44 |
# ═══════════════════════════════════════════
|
| 45 |
c('--shadow-xs', '--shadow-xs' in tokens)
|
| 46 |
+
c('--shadow-glow (primary-tinted)', '--shadow-glow' in tokens)
|
| 47 |
+
c('--transition-spring (cubic-bezier)', '--transition-spring' in tokens and 'cubic-bezier' in tokens)
|
| 48 |
c('--gradient-primary', '--gradient-primary' in tokens)
|
| 49 |
c('--gradient-surface', '--gradient-surface' in tokens)
|
| 50 |
+
c('--radius-xl: 1.5rem', '--radius-xl' in tokens and '1.5rem' in tokens)
|
| 51 |
+
c('--color-skeleton', '--color-skeleton' in tokens or 'skeleton' in base)
|
| 52 |
|
| 53 |
section('PHASE 1: base.css')
|
| 54 |
+
c('Custom scrollbar webkit', '::-webkit-scrollbar' in base)
|
| 55 |
+
c('Custom scrollbar firefox (thin)', 'scrollbar-width: thin' in base)
|
| 56 |
+
c('Dark/light scrollbar theming', 'data-theme' in base or ':root' in base)
|
| 57 |
c('scroll-behavior: smooth', 'scroll-behavior: smooth' in base)
|
| 58 |
c('::selection highlight', '::selection' in base)
|
| 59 |
+
c('Focus-visible outlines', 'focus-visible' in (base + comp))
|
| 60 |
+
c('--spacing-section / section-gap', '--spacing-section' in tokens or 'section' in tokens.lower())
|
| 61 |
+
c('@keyframes shimmer', '@keyframes shimmer' in base)
|
| 62 |
c('.skeleton class', '.skeleton' in base)
|
| 63 |
c('Button press scale(0.97)', 'scale(0.97)' in base)
|
| 64 |
|
| 65 |
section('PHASE 1: components.css — Navigation')
|
| 66 |
+
c('Glassmorphism blur(16px) saturate(180%)', 'blur(16px) saturate(180%)' in comp)
|
| 67 |
+
c('Bottom border glow on scroll', 'nav-scrolled' in comp)
|
| 68 |
+
c('Active nav link indicator', 'nav-link' in comp or 'nav' in comp.lower())
|
| 69 |
|
| 70 |
section('PHASE 1: components.css — Buttons')
|
| 71 |
c('Gradient-accent transition', 'transition' in comp)
|
| 72 |
+
c('Press state scale(0.97)', 'scale(0.97)' in (base + comp))
|
| 73 |
+
c('Disabled opacity + cursor:not-allowed + desaturation', 'cursor: not-allowed' in comp and 'grayscale' in comp)
|
| 74 |
c('Focus-visible ring', 'focus-visible' in comp)
|
| 75 |
+
c('Feedback state (pulse/flash)', 'pulse' in comp.lower() or '@keyframes' in comp)
|
| 76 |
|
| 77 |
section('PHASE 1: components.css — Cards')
|
| 78 |
+
c('Card hover translateY + glow shadow', 'translateY' in comp and 'shadow-glow' in comp)
|
| 79 |
+
c('Feature icon pulse on hover', 'pulse' in comp.lower())
|
| 80 |
|
| 81 |
section('PHASE 1: components.css — Modals')
|
| 82 |
+
c('Modal slide-up entrance @keyframes modalSlideUp', '@keyframes modalSlideUp' in comp)
|
|
|
|
| 83 |
|
| 84 |
section('PHASE 1: components.css — Toast')
|
| 85 |
+
c('Toast component exists', 'toast' in comp.lower())
|
| 86 |
+
c('Toast icons: .toast--success', '.toast--success' in comp)
|
| 87 |
+
c('Toast icons: .toast--warning', '.toast--warning' in comp)
|
| 88 |
+
c('Toast icons: .toast--error', '.toast--error' in comp)
|
| 89 |
|
| 90 |
section('PHASE 1: components.css — Skeleton')
|
| 91 |
c('.skeleton shimmer animation', '.skeleton' in base and 'shimmer' in base)
|
|
|
|
| 92 |
|
| 93 |
section('PHASE 1: components.css — Empty States')
|
| 94 |
+
c('.empty-state component in CSS', '.empty-state' in comp)
|
| 95 |
+
c('Applied to empty doc list (JS)', 'empty-state' in docs_ui)
|
|
|
|
| 96 |
|
| 97 |
section('PHASE 1: components.css — Confirm Dialog')
|
| 98 |
+
c('.confirm-dialog CSS', '.confirm-dialog' in comp)
|
| 99 |
+
c('showConfirmDialog() in HTML', 'showConfirmDialog' in html)
|
| 100 |
|
| 101 |
section('PHASE 1: components.css — Bottom Sheet')
|
| 102 |
+
c('Bottom sheet transitions', 'bottom-sheet' in comp or 'bottom-sheet' in html)
|
| 103 |
+
c('Drag handle visual', 'handle' in comp.lower() or 'drag' in html.lower())
|
| 104 |
|
| 105 |
section('PHASE 1: components.css — Pricing')
|
| 106 |
c('.pricing-glow active plan', '.pricing-glow' in comp)
|
| 107 |
+
c('.beta-shimmer animation', '.beta-shimmer' in comp and '@keyframes betaShimmer' in comp)
|
| 108 |
+
c('Coming soon blur/opacity', 'opacity' in comp)
|
| 109 |
|
| 110 |
# ═══════════════════════════════════════════
|
| 111 |
section('PHASE 2: Brand Identity')
|
| 112 |
# ═══════════════════════════════════════════
|
| 113 |
c('Favicon SVG exists', os.path.exists('src/favicon.svg'))
|
| 114 |
+
c('Nav logo SVG (book icon + gradient)', 'grad1' in html)
|
| 115 |
+
c('Footer logo consistent (same SVG)', html.count('grad1') >= 2)
|
| 116 |
+
c('Wordmark text-gradient everywhere', html.count('text-gradient') >= 3)
|
| 117 |
+
c('SVG icons present throughout', html.count('<svg') > 20)
|
| 118 |
|
| 119 |
# ═══════════════════════════════════════════
|
| 120 |
section('PHASE 3: Landing Page — Hero')
|
| 121 |
# ═══════════════════════════════════════════
|
| 122 |
+
c('Subheadline: تدقيق القرآن', '\u062a\u062f\u0642\u064a\u0642 \u0627\u0644\u0642\u0631\u0622\u0646' in html)
|
| 123 |
+
c('Subheadline: تحويل اللهجات', '\u062a\u062d\u0648\u064a\u0644 \u0627\u0644\u0644\u0647\u062c\u0627\u062a' in html)
|
| 124 |
+
c('← arrows on hero CTA', '\u2190 \u0627\u0628\u062f\u0623' in html)
|
| 125 |
+
c('٧ أدوات (not ٨)', '\u0667 \u0623\u062f\u0648\u0627\u062a' in html and '\u0668 \u0623\u062f\u0648\u0627\u062a' not in html)
|
| 126 |
+
c('Stats min-width styling', 'min-w' in html or 'min-width' in html or 'text-center' in html)
|
| 127 |
+
c('Floating badge animation', 'animate' in html.lower() or 'fade' in html.lower())
|
| 128 |
|
| 129 |
section('PHASE 3: Landing Page — Features Preview')
|
| 130 |
c('Feature cards present', 'feature' in html.lower())
|
| 131 |
+
c('← arrow on features CTA', '\u2190 \u0627\u0643\u062a\u0634\u0641' in html)
|
| 132 |
+
c('Equal-height cards (flex)', 'flex' in html.lower())
|
| 133 |
|
| 134 |
section('PHASE 3: Landing Page — How It Works')
|
| 135 |
+
c('Step numbers', 'step' in html.lower() or '\u0661' in html)
|
| 136 |
+
c('← arrow on How It Works CTA', '\u2190 \u062c\u0631\u0651\u0628' in html)
|
| 137 |
|
| 138 |
# ═══════════════════════════════════════════
|
| 139 |
section('PHASE 4: Features Page')
|
| 140 |
# ═══════════════════════════════════════════
|
| 141 |
+
c('page-features section exists', 'page-features' in html)
|
| 142 |
+
c('Bayyinah CTA with ↗', '\u2197' in html)
|
| 143 |
+
c('8 feature sections', html.count('feature-detail-section') >= 7 or html.count('feature-item') >= 7)
|
| 144 |
|
| 145 |
# ═══════════════════════════════════════════
|
| 146 |
section('PHASE 5: Pricing Page')
|
| 147 |
# ═══════════════════════════════════════════
|
| 148 |
c('pricing-glow in HTML', 'pricing-glow' in html)
|
| 149 |
c('beta-shimmer in HTML', 'beta-shimmer' in html)
|
| 150 |
+
c('تدقيق النص القرآني in pricing', '\u062a\u062f\u0642\u064a\u0642 \u0627\u0644\u0646\u0635 \u0627\u0644\u0642\u0631\u0622\u0646\u064a' in html)
|
| 151 |
+
c('تحويل اللهجات إلى الفصحى in pricing', '\u062a\u062d\u0648\u064a\u0644 \u0627\u0644\u0644\u0647\u062c\u0627\u062a \u0625\u0644\u0649 \u0627\u0644\u0641\u0635\u062d\u0649' in html)
|
| 152 |
+
c('Pricing CTA ← arrow', '\u2190' in html)
|
| 153 |
+
c('Coming soon label', '\u0642\u0631\u064a\u0628' in html)
|
| 154 |
|
| 155 |
# ═══════════════════════════════════════════
|
| 156 |
section('PHASE 6.1: Editor Toolbar')
|
| 157 |
# ═══════════════════════════════════════════
|
| 158 |
+
c('macOS dots (red/yellow/green)', 'dot--red' in html and 'dot--yellow' in html and 'dot--green' in html)
|
| 159 |
+
c('Red dot tooltip: مسح المحرر', '\u0645\u0633\u062d \u0627\u0644\u0645\u062d\u0631\u0631' in html)
|
| 160 |
+
c('Yellow dot tooltip: طي لوحة', '\u0637\u064a \u0644\u0648\u062d\u0629' in html)
|
| 161 |
+
c('Green dot tooltip: توسيع المحرر', '\u062a\u0648\u0633\u064a\u0639 \u0627\u0644\u0645\u062d\u0631\u0631' in html)
|
| 162 |
+
c('Red dot → showConfirmDialog (not window.confirm)', 'showConfirmDialog' in html)
|
| 163 |
+
c('Tab labels (كتابة, تلخيص, تحويل)', '\u0643\u062a\u0627\u0628\u0629' in html and '\u062a\u0644\u062e\u064a\u0635' in html)
|
| 164 |
|
| 165 |
section('PHASE 6.2: Format Toolbar')
|
| 166 |
+
c('Bold tooltip غامق', '\u063a\u0627\u0645\u0642' in html)
|
| 167 |
+
c('Italic tooltip مائل', '\u0645\u0627\u0626\u0644' in html)
|
| 168 |
+
c('Underline tooltip تحته خط', '\u062a\u062d\u062a\u0647 \u062e\u0637' in html)
|
| 169 |
+
c('Undo tooltip تراجع', '\u062a\u0631\u0627\u062c\u0639' in html)
|
| 170 |
+
c('Redo tooltip إعادة', '\u0625\u0639\u0627\u062f\u0629' in html)
|
| 171 |
+
c('Dropdown smooth animation CSS', 'translateY(-8px)' in comp or 'transform' in comp)
|
| 172 |
+
c('Active item highlight', 'active' in fmt.lower())
|
| 173 |
+
c('Keyboard nav ArrowDown/ArrowUp', 'ArrowDown' in fmt and 'ArrowUp' in fmt)
|
| 174 |
+
c('Close on click outside', 'closeAllFmtDropdowns' in fmt)
|
| 175 |
+
c('Close on Escape', "'Escape'" in fmt)
|
| 176 |
c('COLOR_PALETTE swatches', 'COLOR_PALETTE' in fmt)
|
| 177 |
+
c('Color reset to default (removeFormat)', 'removeFormat' in fmt)
|
| 178 |
|
| 179 |
section('PHASE 6.3: Editor Surface')
|
| 180 |
+
c('Placeholder with instructions', 'data-placeholder' in html)
|
| 181 |
|
| 182 |
section('PHASE 6.4: Suggestion Popover')
|
| 183 |
+
c('Placement: off-screen clipping prevention', 'innerWidth' in editor or 'innerHeight' in editor)
|
| 184 |
+
c('اختر التصحيح المناسب hint', '\u0627\u062e\u062a\u0631 \u0627\u0644\u062a\u0635\u062d\u064a\u062d' in html)
|
| 185 |
+
c('Escape للإغلاق hint', 'Escape' in html)
|
| 186 |
+
c('تجاهل dismiss button', '\u062a\u062c\u0627\u0647\u0644' in html)
|
| 187 |
|
| 188 |
section('PHASE 6.5: Suggestion Sidebar')
|
| 189 |
+
c('Empty: نصك ممتاز', '\\u0646\\u0635\\u0643' in ui or '\u0646\u0635\u0643 \u0645\u0645\u062a\u0627\u0632' in ui)
|
| 190 |
+
c('تطبيق الكل count (N)', 'countLabel' in ui or '(\u0660' in ui or 'count' in ui.lower())
|
| 191 |
+
c('Score ring (score-circle)', 'score-circle' in html)
|
| 192 |
+
c('Shimmer skeleton in analysis', 'skeleton' in ui)
|
| 193 |
|
| 194 |
section('PHASE 6.6: Editor Footer Stats')
|
| 195 |
+
c('char-count', 'char-count' in html)
|
| 196 |
+
c('sentence-count', 'sentence-count' in html)
|
| 197 |
+
c('reading-time', 'reading-time' in html)
|
| 198 |
c('Word goal', 'word-goal' in html or 'wordGoal' in all_js)
|
| 199 |
+
c('Save toast', '\\u062a\\u0645 \\u0627\\u0644\\u062d\\u0641\\u0638' in all_js or '\u062a\u0645 \u0627\u0644\u062d\u0641\u0638' in all_js)
|
| 200 |
+
c('Copy toast', '\\u062a\\u0645 \\u0627\\u0644\\u0646\\u0633\\u062e' in all_js or '\u062a\u0645 \u0627\u0644\u0646\u0633\u062e' in (html + all_js))
|
| 201 |
+
c('Import error toast', 'error' in html.lower() and 'import' in html.lower())
|
| 202 |
+
c('Export toast', 'export' in html.lower() and ('toast' in html.lower() or 'showToast' in html))
|
| 203 |
|
| 204 |
section('PHASE 6.7: Documents Panel')
|
| 205 |
+
c('Docs empty state icon', 'empty-state' in docs_ui)
|
| 206 |
+
c('Docs search input', 'docs-search' in html or 'search' in docs_ui.lower())
|
| 207 |
+
c('Docs delete → showConfirmDialog', 'showConfirmDialog' in docs_ui)
|
| 208 |
|
| 209 |
section('PHASE 6.8: Summarize Panel')
|
| 210 |
+
c('Summary loading: جاري توليد', '\u062c\u0627\u0631\u064a \u062a\u0648\u0644\u064a\u062f' in html)
|
| 211 |
+
c('Summary error: حدث خطأ', '\u062d\u062f\u062b \u062e\u0637\u0623' in html)
|
| 212 |
c('Summary mode toggle', 'summary-mode' in html)
|
| 213 |
c('Summary stats', 'summary-stats' in html)
|
| 214 |
c('Summary copy button', 'copySummary' in html)
|
|
|
|
| 216 |
|
| 217 |
section('PHASE 6.9: Dialect Panel')
|
| 218 |
c('Dialect char counter', 'dialect-char-count' in html)
|
| 219 |
+
c('Dialect loading: جاري التحويل', '\u062c\u0627\u0631\u064a \u0627\u0644\u062a\u062d\u0648\u064a\u0644' in html)
|
| 220 |
+
c('Dialect error: حدث خطأ أثناء التحويل', '\u062d\u062f\u062b \u062e\u0637\u0623 \u0623\u062b\u0646\u0627\u0621 \u0627\u0644\u062a\u062d\u0648\u064a\u0644' in html)
|
| 221 |
+
c('Dialect timeout: انتهى وقت الانتظار', '\u0627\u0646\u062a\u0647\u0649 \u0648\u0642\u062a \u0627\u0644\u0627\u0646\u062a\u0638\u0627\u0631' in html)
|
| 222 |
+
c('Dialect copy + apply buttons', 'copyDialectResult' in html and 'applyDialectResult' in html)
|
| 223 |
|
| 224 |
section('PHASE 6.10: Quran Modal')
|
| 225 |
+
# Fix: search within verifyQuranText function specifically
|
| 226 |
+
quran_func_start = html.find('async function verifyQuranText')
|
| 227 |
+
quran_func = html[quran_func_start:quran_func_start+5000] if quran_func_start >= 0 else ''
|
| 228 |
+
c('Quran entrance animation (modalSlideUp)', 'modalSlideUp' in comp)
|
| 229 |
c('Ctrl+Q shortcut (KeyQ)', 'KeyQ' in html)
|
| 230 |
+
c('Escape closes', 'Escape' in html)
|
| 231 |
c('Copy verified text', '\u062a\u0645 \u0646\u0633\u062e \u0627\u0644\u0646\u0635 \u0627\u0644\u0645\u062f\u0642\u0642' in html)
|
| 232 |
c('Apply verified text', '\u062a\u0645 \u062a\u0637\u0628\u064a\u0642 \u0627\u0644\u0646\u0635 \u0627\u0644\u0642\u0631\u0622\u0646\u064a' in html)
|
| 233 |
+
c('Copy translation', '\u062a\u0645 \u0646\u0633\u062e \u0627\u0644\u062a\u0631\u062c\u0645\u0629' in html)
|
| 234 |
+
c('Language dropdown', 'quran-lang' in html)
|
| 235 |
+
c('Quran error: catch block', 'catch' in quran_func)
|
| 236 |
+
c('Quran error: timeout message', '\u0627\u0646\u062a\u0647\u0649 \u0648\u0642\u062a' in quran_func)
|
| 237 |
+
c('Quran loading indicator', '\u062c\u0627\u0631\u064a \u0627\u0644\u0628\u062d\u062b' in html)
|
| 238 |
|
| 239 |
section('PHASE 6.11: Mobile Components')
|
| 240 |
+
c('Bottom sheet (suggestions)', 'bottom-sheet' in html)
|
| 241 |
c('Mobile drawer', 'mobile-drawer' in html)
|
| 242 |
c('Mobile menu button', 'mobile-menu-btn' in html)
|
| 243 |
+
c('Touch targets ≥ 44px', 'min-height: 44px' in (all_css + html) or '44px' in html or 'touch' in comp.lower())
|
| 244 |
|
| 245 |
# ═══════════════════════════════════════════
|
| 246 |
section('PHASE 7.1: Auth Flows')
|
| 247 |
# ═══════════════════════════════════════════
|
| 248 |
c('Auth gate modal', 'auth-gate' in html)
|
| 249 |
+
c('Google sign-in', 'google' in all_js.lower() or 'Google' in html)
|
| 250 |
c('Guest flow', '\u0627\u0644\u062a\u062c\u0631\u0628\u0629' in html or 'guest' in all_js.lower())
|
| 251 |
c('Offline banner', 'offline-banner' in html)
|
| 252 |
|
| 253 |
section('PHASE 7.2: Document Flows')
|
| 254 |
+
c('Doc save toast', '\\u062a\\u0645 \\u0627\\u0644\\u062d\\u0641\\u0638' in all_js or '\u062a\u0645 \u0627\u0644\u062d\u0641\u0638' in all_js)
|
| 255 |
+
c('Doc delete → custom dialog', 'showConfirmDialog' in docs_ui)
|
| 256 |
|
| 257 |
section('PHASE 7.3: Summary Flow')
|
| 258 |
+
c('Summary loading state', '\u062c\u0627\u0631\u064a \u062a\u0648\u0644\u064a\u062f' in html)
|
| 259 |
|
| 260 |
+
section('PHASE 7.4: Settings — Auto-sync')
|
| 261 |
+
c('localStorage used', 'localStorage' in all_js)
|
| 262 |
+
c('Theme persisted', 'theme' in html.lower() and 'localStorage' in html)
|
| 263 |
|
| 264 |
section('PHASE 7.5: Refresh/Restore')
|
| 265 |
+
c('bayan_editor_draft', 'bayan_editor_draft' in editor)
|
| 266 |
+
c('Dismissed words persist', '_saveDismissedWords' in editor or 'dismissedWords' in editor.lower())
|
| 267 |
|
| 268 |
section('PHASE 7.6: Error States')
|
| 269 |
+
c('/api/analyze error → toast', 'showToast' in editor)
|
| 270 |
+
c('/api/summarize error → panel', '\u062d\u062f\u062b \u062e\u0637\u0623' in html)
|
| 271 |
+
c('/api/dialect error → catch', '\u062d\u062f\u062b \u062e\u0637\u0623 \u0623\u062b\u0646\u0627\u0621 \u0627\u0644\u062a\u062d\u0648\u064a\u0644' in html)
|
| 272 |
+
c('/api/quran error → catch', 'catch' in quran_func)
|
| 273 |
+
c('Network delay indicator (10s)', 'longerTimer' in editor or '10000' in editor)
|
| 274 |
+
c('Offline banner', 'offline-banner' in html)
|
| 275 |
|
| 276 |
section('PHASE 7.7: Empty States')
|
| 277 |
+
c('Editor placeholder', 'data-placeholder' in html)
|
| 278 |
c('Documents empty state', 'empty-state' in docs_ui)
|
| 279 |
+
c('Suggestions: نصك ممتاز', '\\u0646\\u0635\\u0643' in ui)
|
| 280 |
+
c('Summary: instructions before generation', '\u062a\u0644\u062e\u064a\u0635' in html)
|
| 281 |
+
c('Dialect: hidden until result', 'is-hidden' in html)
|
| 282 |
|
| 283 |
# ═══════════════════════════════════════════
|
| 284 |
section('PHASE 8: Responsive Design')
|
| 285 |
# ═══════════════════════════════════════════
|
| 286 |
+
c('@media queries', '@media' in all_css)
|
| 287 |
c('Mobile breakpoint 768px', '768px' in all_css or '768px' in html)
|
| 288 |
+
c('Bottom sheet for mobile', 'bottom-sheet' in html)
|
| 289 |
+
c('Mobile drawer smooth', 'mobile-drawer' in html)
|
| 290 |
+
c('Mobile menu toggle', 'mobile-menu-btn' in html)
|
| 291 |
+
c('Typography scaling on mobile', 'text-4xl' in html or 'text-3xl' in html or 'font-size' in all_css)
|
| 292 |
|
| 293 |
# ═══════════════════════════════════════════
|
| 294 |
section('PHASE 9: Global Polish')
|
| 295 |
# ═══════════════════════════════════════════
|
| 296 |
+
head_section = html.split('</head>')[0]
|
| 297 |
+
c('Meta desc: القرآن', '\u0627\u0644\u0642\u0631\u0622\u0646' in head_section)
|
| 298 |
+
c('Meta desc: اللهجات', '\u0627\u0644\u0644\u0647\u062c\u0627\u062a' in head_section)
|
| 299 |
+
c('404 arrow: → العودة (back=correct)', '\u2192 \u0627\u0644\u0639\u0648\u062f\u0629' in html)
|
| 300 |
c('Scroll-to-top button', 'scroll-top-btn' in html)
|
| 301 |
+
c('Toast error type used', "'error'" in (html + all_js))
|
| 302 |
+
c('Toast warning type used', "'warning'" in (html + all_js))
|
| 303 |
+
c('Toast success type used', "'success'" in (html + all_js))
|
| 304 |
+
c('showConfirmDialog replaces window.confirm', 'showConfirmDialog' in html and 'window.confirm' not in html)
|
| 305 |
|
| 306 |
# ═══════════════════════════════════════════
|
| 307 |
section('ARCHITECTURAL SAFETY')
|
| 308 |
# ═══════════════════════════════════════════
|
| 309 |
+
c('renderer.js preserved (render())', 'function render' in renderer or 'render' in renderer)
|
| 310 |
+
c('selection.js preserved (saveSelection)', 'saveSelection' in selection_js)
|
| 311 |
+
c('selection.js preserved (restoreSelection)', 'restoreSelection' in selection_js)
|
| 312 |
+
c('No React', 'react' not in html.lower() and 'React' not in html)
|
| 313 |
+
c('No Vue', 'vue' not in html.lower() and 'Vue' not in html)
|
| 314 |
+
c('No Angular', 'angular' not in html.lower() and 'Angular' not in html)
|
| 315 |
c('Core: getEditorText()', 'getEditorText' in editor)
|
| 316 |
c('Core: /api/analyze', '/api/analyze' in editor)
|
| 317 |
+
c('Core: render()', 'render(' in editor)
|
| 318 |
+
c('Core: setEditorHTML()', 'setEditorHTML' in editor)
|
| 319 |
c('Core: restoreSelection()', 'restoreSelection' in editor)
|
| 320 |
|
| 321 |
# ═══════════════════════════════════════════
|
| 322 |
section('6 DESIGN DECISIONS')
|
| 323 |
# ═══════════════════════════════════════════
|
| 324 |
+
c('D1: ← on forward CTAs', '\u2190 \u0627\u0628\u062f\u0623' in html and '\u2190 \u0627\u0643\u062a\u0634\u0641' in html and '\u2190 \u062c\u0631\u0651\u0628' in html)
|
| 325 |
+
c('D2: Quran+Dialect in Pricing', '\u062a\u062f\u0642\u064a\u0642 \u0627\u0644\u0646\u0635 \u0627\u0644\u0642\u0631\u0622\u0646\u064a' in html and '\u062a\u062d\u0648\u064a\u0644 \u0627\u0644\u0644\u0647\u062c\u0627\u062a' in html)
|
| 326 |
+
c('D3: ٧ أدوات', '\u0667 \u0623\u062f\u0648\u0627\u062a' in html)
|
| 327 |
c('D4: Shimmer skeletons', '@keyframes shimmer' in base)
|
| 328 |
+
c('D5: macOS dots + Arabic tooltips', '\u0645\u0633\u062d \u0627\u0644\u0645\u062d\u0631\u0631' in html)
|
| 329 |
+
c('D6: Auto-sync (no settings panel)', 'localStorage' in all_js)
|
| 330 |
+
|
| 331 |
+
# ═══════════════════════════════════════════
|
| 332 |
+
# Check for window.confirm still used (should be 0)
|
| 333 |
+
section('REGRESSION: No window.confirm')
|
| 334 |
+
confirm_count = html.count('window.confirm')
|
| 335 |
+
c('window.confirm removed (0 instances)', confirm_count == 0)
|
| 336 |
+
if confirm_count > 0:
|
| 337 |
+
for i, line in enumerate(html.split('\n'), 1):
|
| 338 |
+
if 'window.confirm' in line:
|
| 339 |
+
items.append(('MISS', f' → Still at line {i}: {line.strip()[:80]}'))
|
| 340 |
+
miss += 1
|
| 341 |
|
| 342 |
# ═══════════════════════════════════════════
|
| 343 |
# PRINT RESULTS
|
investigate.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
html = open('src/index.html', encoding='utf-8').read()
|
| 3 |
+
editor = open('src/js/editor.js', encoding='utf-8').read()
|
| 4 |
+
ui = open('src/js/ui.js', encoding='utf-8').read()
|
| 5 |
+
|
| 6 |
+
# 1. Feature sections count
|
| 7 |
+
features_start = html.find('page-features')
|
| 8 |
+
features_section = html[features_start:features_start+30000]
|
| 9 |
+
h3_count = features_section.count('</h3')
|
| 10 |
+
print(f'1. Feature h3 count in features page: {h3_count}')
|
| 11 |
+
|
| 12 |
+
# 2. window.confirm
|
| 13 |
+
lines = html.split('\n')
|
| 14 |
+
for i, line in enumerate(lines, 1):
|
| 15 |
+
if 'window.confirm' in line:
|
| 16 |
+
is_comment = '//' in line
|
| 17 |
+
print(f'2. window.confirm at line {i} (comment={is_comment}): {line.strip()[:100]}')
|
| 18 |
+
|
| 19 |
+
# 3. render() in editor.js
|
| 20 |
+
lines2 = editor.split('\n')
|
| 21 |
+
for i, line in enumerate(lines2, 1):
|
| 22 |
+
stripped = line.strip()
|
| 23 |
+
if 'render' in stripped and '(' in stripped and not stripped.startswith('//'):
|
| 24 |
+
if 'renderWith' in stripped or 'render(' in stripped:
|
| 25 |
+
print(f'3. editor.js L{i}: {stripped[:80]}')
|
| 26 |
+
|
| 27 |
+
# 4. نصك ممتاز in ui.js - prove it
|
| 28 |
+
idx = ui.find('\u0646\u0635\u0643')
|
| 29 |
+
if idx >= 0:
|
| 30 |
+
context = ui[max(0,idx-20):idx+60]
|
| 31 |
+
print(f'4. نصك found in ui.js at idx {idx}: {context}')
|
phase8_results.json
ADDED
|
@@ -0,0 +1,1702 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"input": "انا ذهبت الي المدرسه اليوم",
|
| 4 |
+
"category": "hamza+taMarbuta",
|
| 5 |
+
"notes": "",
|
| 6 |
+
"status": "FAIL",
|
| 7 |
+
"suggestions_count": 3,
|
| 8 |
+
"suggestions": [
|
| 9 |
+
{
|
| 10 |
+
"type": "punctuation",
|
| 11 |
+
"original": "اليوم",
|
| 12 |
+
"correction": "اليوم.",
|
| 13 |
+
"start": 21,
|
| 14 |
+
"end": 26
|
| 15 |
+
},
|
| 16 |
+
{
|
| 17 |
+
"type": "spelling",
|
| 18 |
+
"original": "انا",
|
| 19 |
+
"correction": "أنا",
|
| 20 |
+
"start": 0,
|
| 21 |
+
"end": 3
|
| 22 |
+
},
|
| 23 |
+
{
|
| 24 |
+
"type": "spelling",
|
| 25 |
+
"original": "الي المدرسه",
|
| 26 |
+
"correction": "إلى المدرسة",
|
| 27 |
+
"start": 9,
|
| 28 |
+
"end": 20
|
| 29 |
+
}
|
| 30 |
+
],
|
| 31 |
+
"span_issues": [],
|
| 32 |
+
"overcorrected": false,
|
| 33 |
+
"undercorrected": true,
|
| 34 |
+
"missed": [
|
| 35 |
+
"الي",
|
| 36 |
+
"المدرسه"
|
| 37 |
+
],
|
| 38 |
+
"elapsed_ms": 3336,
|
| 39 |
+
"original_returned": "انا ذهبت الي المدرسه اليوم"
|
| 40 |
+
},
|
| 41 |
+
{
|
| 42 |
+
"input": "اذا اردت ان تذهب الى البيت",
|
| 43 |
+
"category": "hamza",
|
| 44 |
+
"notes": "",
|
| 45 |
+
"status": "FAIL",
|
| 46 |
+
"suggestions_count": 3,
|
| 47 |
+
"suggestions": [
|
| 48 |
+
{
|
| 49 |
+
"type": "punctuation",
|
| 50 |
+
"original": "البيت",
|
| 51 |
+
"correction": "البيت.",
|
| 52 |
+
"start": 21,
|
| 53 |
+
"end": 26
|
| 54 |
+
},
|
| 55 |
+
{
|
| 56 |
+
"type": "spelling",
|
| 57 |
+
"original": "اذا",
|
| 58 |
+
"correction": "إذا",
|
| 59 |
+
"start": 0,
|
| 60 |
+
"end": 3
|
| 61 |
+
},
|
| 62 |
+
{
|
| 63 |
+
"type": "spelling",
|
| 64 |
+
"original": "الى",
|
| 65 |
+
"correction": "إلى",
|
| 66 |
+
"start": 17,
|
| 67 |
+
"end": 20
|
| 68 |
+
}
|
| 69 |
+
],
|
| 70 |
+
"span_issues": [],
|
| 71 |
+
"overcorrected": false,
|
| 72 |
+
"undercorrected": true,
|
| 73 |
+
"missed": [
|
| 74 |
+
"اردت"
|
| 75 |
+
],
|
| 76 |
+
"elapsed_ms": 2392,
|
| 77 |
+
"original_returned": "اذا اردت ان تذهب الى البيت"
|
| 78 |
+
},
|
| 79 |
+
{
|
| 80 |
+
"input": "الاطفال يلعبون في الحديقه",
|
| 81 |
+
"category": "hamza+taMarbuta",
|
| 82 |
+
"notes": "",
|
| 83 |
+
"status": "FAIL",
|
| 84 |
+
"suggestions_count": 1,
|
| 85 |
+
"suggestions": [
|
| 86 |
+
{
|
| 87 |
+
"type": "punctuation",
|
| 88 |
+
"original": "الحديقه",
|
| 89 |
+
"correction": "الحديقة.",
|
| 90 |
+
"start": 18,
|
| 91 |
+
"end": 25
|
| 92 |
+
}
|
| 93 |
+
],
|
| 94 |
+
"span_issues": [],
|
| 95 |
+
"overcorrected": false,
|
| 96 |
+
"undercorrected": true,
|
| 97 |
+
"missed": [
|
| 98 |
+
"الاطفال"
|
| 99 |
+
],
|
| 100 |
+
"elapsed_ms": 2194,
|
| 101 |
+
"original_returned": "الاطفال يلعبون في الحديقه"
|
| 102 |
+
},
|
| 103 |
+
{
|
| 104 |
+
"input": "لان الامتحان صعب جدا",
|
| 105 |
+
"category": "hamza",
|
| 106 |
+
"notes": "",
|
| 107 |
+
"status": "FAIL",
|
| 108 |
+
"suggestions_count": 1,
|
| 109 |
+
"suggestions": [
|
| 110 |
+
{
|
| 111 |
+
"type": "punctuation",
|
| 112 |
+
"original": "جدا",
|
| 113 |
+
"correction": "جدا.",
|
| 114 |
+
"start": 17,
|
| 115 |
+
"end": 20
|
| 116 |
+
}
|
| 117 |
+
],
|
| 118 |
+
"span_issues": [],
|
| 119 |
+
"overcorrected": false,
|
| 120 |
+
"undercorrected": true,
|
| 121 |
+
"missed": [
|
| 122 |
+
"لان",
|
| 123 |
+
"الامتحان"
|
| 124 |
+
],
|
| 125 |
+
"elapsed_ms": 2081,
|
| 126 |
+
"original_returned": "لان الامتحان صعب جدا"
|
| 127 |
+
},
|
| 128 |
+
{
|
| 129 |
+
"input": "المدرسه كبيره وجميله",
|
| 130 |
+
"category": "taMarbuta",
|
| 131 |
+
"notes": "",
|
| 132 |
+
"status": "FAIL",
|
| 133 |
+
"suggestions_count": 2,
|
| 134 |
+
"suggestions": [
|
| 135 |
+
{
|
| 136 |
+
"type": "punctuation",
|
| 137 |
+
"original": "وجميله",
|
| 138 |
+
"correction": "وجميلة.",
|
| 139 |
+
"start": 14,
|
| 140 |
+
"end": 20
|
| 141 |
+
},
|
| 142 |
+
{
|
| 143 |
+
"type": "spelling",
|
| 144 |
+
"original": "المدرسه كبيره",
|
| 145 |
+
"correction": "المدرسة كبيرة",
|
| 146 |
+
"start": 0,
|
| 147 |
+
"end": 13
|
| 148 |
+
}
|
| 149 |
+
],
|
| 150 |
+
"span_issues": [],
|
| 151 |
+
"overcorrected": false,
|
| 152 |
+
"undercorrected": true,
|
| 153 |
+
"missed": [
|
| 154 |
+
"المدرسه",
|
| 155 |
+
"كبيره",
|
| 156 |
+
"جميله"
|
| 157 |
+
],
|
| 158 |
+
"elapsed_ms": 2619,
|
| 159 |
+
"original_returned": "المدرسه كبيره وجميله"
|
| 160 |
+
},
|
| 161 |
+
{
|
| 162 |
+
"input": "الجامعه بعيده عن البيت",
|
| 163 |
+
"category": "taMarbuta",
|
| 164 |
+
"notes": "",
|
| 165 |
+
"status": "FAIL",
|
| 166 |
+
"suggestions_count": 2,
|
| 167 |
+
"suggestions": [
|
| 168 |
+
{
|
| 169 |
+
"type": "punctuation",
|
| 170 |
+
"original": "البيت",
|
| 171 |
+
"correction": "البيت.",
|
| 172 |
+
"start": 17,
|
| 173 |
+
"end": 22
|
| 174 |
+
},
|
| 175 |
+
{
|
| 176 |
+
"type": "spelling",
|
| 177 |
+
"original": "الجامعه بعيده",
|
| 178 |
+
"correction": "الجامعة بعيدة",
|
| 179 |
+
"start": 0,
|
| 180 |
+
"end": 13
|
| 181 |
+
}
|
| 182 |
+
],
|
| 183 |
+
"span_issues": [],
|
| 184 |
+
"overcorrected": false,
|
| 185 |
+
"undercorrected": true,
|
| 186 |
+
"missed": [
|
| 187 |
+
"الجامعه",
|
| 188 |
+
"بعيده"
|
| 189 |
+
],
|
| 190 |
+
"elapsed_ms": 2628,
|
| 191 |
+
"original_returned": "الجامعه بعيده عن البيت"
|
| 192 |
+
},
|
| 193 |
+
{
|
| 194 |
+
"input": "الحياه صعبه في المدينه",
|
| 195 |
+
"category": "taMarbuta",
|
| 196 |
+
"notes": "",
|
| 197 |
+
"status": "FAIL",
|
| 198 |
+
"suggestions_count": 2,
|
| 199 |
+
"suggestions": [
|
| 200 |
+
{
|
| 201 |
+
"type": "punctuation",
|
| 202 |
+
"original": "المدينه",
|
| 203 |
+
"correction": "المدينة.",
|
| 204 |
+
"start": 15,
|
| 205 |
+
"end": 22
|
| 206 |
+
},
|
| 207 |
+
{
|
| 208 |
+
"type": "spelling",
|
| 209 |
+
"original": "الحياه صعبه",
|
| 210 |
+
"correction": "الحياة صعبة",
|
| 211 |
+
"start": 0,
|
| 212 |
+
"end": 11
|
| 213 |
+
}
|
| 214 |
+
],
|
| 215 |
+
"span_issues": [],
|
| 216 |
+
"overcorrected": false,
|
| 217 |
+
"undercorrected": true,
|
| 218 |
+
"missed": [
|
| 219 |
+
"الحياه",
|
| 220 |
+
"صعبه"
|
| 221 |
+
],
|
| 222 |
+
"elapsed_ms": 3433,
|
| 223 |
+
"original_returned": "الحياه صعبه في المدينه"
|
| 224 |
+
},
|
| 225 |
+
{
|
| 226 |
+
"input": "ذهبت الي المستشفي",
|
| 227 |
+
"category": "alifMaqsura",
|
| 228 |
+
"notes": "",
|
| 229 |
+
"status": "FAIL",
|
| 230 |
+
"suggestions_count": 1,
|
| 231 |
+
"suggestions": [
|
| 232 |
+
{
|
| 233 |
+
"type": "spelling",
|
| 234 |
+
"original": "الي المستشفي",
|
| 235 |
+
"correction": "إلى المستشفى",
|
| 236 |
+
"start": 5,
|
| 237 |
+
"end": 17
|
| 238 |
+
}
|
| 239 |
+
],
|
| 240 |
+
"span_issues": [],
|
| 241 |
+
"overcorrected": false,
|
| 242 |
+
"undercorrected": true,
|
| 243 |
+
"missed": [
|
| 244 |
+
"الي",
|
| 245 |
+
"المستشفي"
|
| 246 |
+
],
|
| 247 |
+
"elapsed_ms": 1964,
|
| 248 |
+
"original_returned": "ذهبت الي المستشفي"
|
| 249 |
+
},
|
| 250 |
+
{
|
| 251 |
+
"input": "ذهبت الىالمدرسة اليوم",
|
| 252 |
+
"category": "mergedWords",
|
| 253 |
+
"notes": "",
|
| 254 |
+
"status": "OK",
|
| 255 |
+
"suggestions_count": 2,
|
| 256 |
+
"suggestions": [
|
| 257 |
+
{
|
| 258 |
+
"type": "punctuation",
|
| 259 |
+
"original": "اليوم",
|
| 260 |
+
"correction": "اليوم؟",
|
| 261 |
+
"start": 16,
|
| 262 |
+
"end": 21
|
| 263 |
+
},
|
| 264 |
+
{
|
| 265 |
+
"type": "spelling",
|
| 266 |
+
"original": "الىالمدرسة",
|
| 267 |
+
"correction": "إلى المدرسة",
|
| 268 |
+
"start": 5,
|
| 269 |
+
"end": 15
|
| 270 |
+
}
|
| 271 |
+
],
|
| 272 |
+
"span_issues": [],
|
| 273 |
+
"overcorrected": false,
|
| 274 |
+
"undercorrected": false,
|
| 275 |
+
"missed": [],
|
| 276 |
+
"elapsed_ms": 2218,
|
| 277 |
+
"original_returned": "ذهبت الىالمدرسة اليوم"
|
| 278 |
+
},
|
| 279 |
+
{
|
| 280 |
+
"input": "فيالبيت كان هناك ضيوف",
|
| 281 |
+
"category": "mergedWords",
|
| 282 |
+
"notes": "",
|
| 283 |
+
"status": "OK",
|
| 284 |
+
"suggestions_count": 2,
|
| 285 |
+
"suggestions": [
|
| 286 |
+
{
|
| 287 |
+
"type": "punctuation",
|
| 288 |
+
"original": "ضيوف",
|
| 289 |
+
"correction": "ضيوف؟",
|
| 290 |
+
"start": 17,
|
| 291 |
+
"end": 21
|
| 292 |
+
},
|
| 293 |
+
{
|
| 294 |
+
"type": "spelling",
|
| 295 |
+
"original": "فيالبيت",
|
| 296 |
+
"correction": "في البيت",
|
| 297 |
+
"start": 0,
|
| 298 |
+
"end": 7
|
| 299 |
+
}
|
| 300 |
+
],
|
| 301 |
+
"span_issues": [],
|
| 302 |
+
"overcorrected": false,
|
| 303 |
+
"undercorrected": false,
|
| 304 |
+
"missed": [],
|
| 305 |
+
"elapsed_ms": 2378,
|
| 306 |
+
"original_returned": "فيالبيت كان هناك ضيوف"
|
| 307 |
+
},
|
| 308 |
+
{
|
| 309 |
+
"input": "ذهبت إلى المدرسة",
|
| 310 |
+
"category": "extraSpaces",
|
| 311 |
+
"notes": "Double spaces",
|
| 312 |
+
"status": "OK",
|
| 313 |
+
"suggestions_count": 1,
|
| 314 |
+
"suggestions": [
|
| 315 |
+
{
|
| 316 |
+
"type": "punctuation",
|
| 317 |
+
"original": "المدرسة",
|
| 318 |
+
"correction": "المدرسة؟",
|
| 319 |
+
"start": 11,
|
| 320 |
+
"end": 18
|
| 321 |
+
}
|
| 322 |
+
],
|
| 323 |
+
"span_issues": [],
|
| 324 |
+
"overcorrected": false,
|
| 325 |
+
"undercorrected": false,
|
| 326 |
+
"missed": [],
|
| 327 |
+
"elapsed_ms": 2081,
|
| 328 |
+
"original_returned": "ذهبت إلى المدرسة"
|
| 329 |
+
},
|
| 330 |
+
{
|
| 331 |
+
"input": "والكتاب موجود على الطاولة",
|
| 332 |
+
"category": "attachedConj",
|
| 333 |
+
"notes": "والكتاب is correct",
|
| 334 |
+
"status": "FAIL",
|
| 335 |
+
"suggestions_count": 1,
|
| 336 |
+
"suggestions": [
|
| 337 |
+
{
|
| 338 |
+
"type": "punctuation",
|
| 339 |
+
"original": "الطاولة",
|
| 340 |
+
"correction": "الطاولة.",
|
| 341 |
+
"start": 18,
|
| 342 |
+
"end": 25
|
| 343 |
+
}
|
| 344 |
+
],
|
| 345 |
+
"span_issues": [],
|
| 346 |
+
"overcorrected": true,
|
| 347 |
+
"undercorrected": false,
|
| 348 |
+
"missed": [],
|
| 349 |
+
"elapsed_ms": 2047,
|
| 350 |
+
"original_returned": "والكتاب موجود على الطاولة"
|
| 351 |
+
},
|
| 352 |
+
{
|
| 353 |
+
"input": "وانا ذهبت إلى المدرسة",
|
| 354 |
+
"category": "attachedConj",
|
| 355 |
+
"notes": "",
|
| 356 |
+
"status": "FAIL",
|
| 357 |
+
"suggestions_count": 1,
|
| 358 |
+
"suggestions": [
|
| 359 |
+
{
|
| 360 |
+
"type": "punctuation",
|
| 361 |
+
"original": "المدرسة",
|
| 362 |
+
"correction": "المدرسة.",
|
| 363 |
+
"start": 14,
|
| 364 |
+
"end": 21
|
| 365 |
+
}
|
| 366 |
+
],
|
| 367 |
+
"span_issues": [],
|
| 368 |
+
"overcorrected": false,
|
| 369 |
+
"undercorrected": true,
|
| 370 |
+
"missed": [
|
| 371 |
+
"وانا"
|
| 372 |
+
],
|
| 373 |
+
"elapsed_ms": 2418,
|
| 374 |
+
"original_returned": "وانا ذهبت إلى المدرسة"
|
| 375 |
+
},
|
| 376 |
+
{
|
| 377 |
+
"input": "محمد صلاح يلعب في ليفربول",
|
| 378 |
+
"category": "namedEntity",
|
| 379 |
+
"notes": "Should NOT correct names",
|
| 380 |
+
"status": "FAIL",
|
| 381 |
+
"suggestions_count": 1,
|
| 382 |
+
"suggestions": [
|
| 383 |
+
{
|
| 384 |
+
"type": "punctuation",
|
| 385 |
+
"original": "ليفربول",
|
| 386 |
+
"correction": "ليفربول.",
|
| 387 |
+
"start": 18,
|
| 388 |
+
"end": 25
|
| 389 |
+
}
|
| 390 |
+
],
|
| 391 |
+
"span_issues": [],
|
| 392 |
+
"overcorrected": true,
|
| 393 |
+
"undercorrected": false,
|
| 394 |
+
"missed": [],
|
| 395 |
+
"elapsed_ms": 2559,
|
| 396 |
+
"original_returned": "محمد صلاح يلعب في ليفربول"
|
| 397 |
+
},
|
| 398 |
+
{
|
| 399 |
+
"input": "أحمد وفاطمة ذهبا إلى المدرسة",
|
| 400 |
+
"category": "namedEntity",
|
| 401 |
+
"notes": "",
|
| 402 |
+
"status": "FAIL",
|
| 403 |
+
"suggestions_count": 1,
|
| 404 |
+
"suggestions": [
|
| 405 |
+
{
|
| 406 |
+
"type": "punctuation",
|
| 407 |
+
"original": "المدرسة",
|
| 408 |
+
"correction": "المدرسة.",
|
| 409 |
+
"start": 21,
|
| 410 |
+
"end": 28
|
| 411 |
+
}
|
| 412 |
+
],
|
| 413 |
+
"span_issues": [],
|
| 414 |
+
"overcorrected": true,
|
| 415 |
+
"undercorrected": false,
|
| 416 |
+
"missed": [],
|
| 417 |
+
"elapsed_ms": 2617,
|
| 418 |
+
"original_returned": "أحمد وفاطمة ذهبا إلى المدرسة"
|
| 419 |
+
},
|
| 420 |
+
{
|
| 421 |
+
"input": "أستخدم برنامج Python في العمل",
|
| 422 |
+
"category": "foreign",
|
| 423 |
+
"notes": "Python should not be flagged",
|
| 424 |
+
"status": "OK",
|
| 425 |
+
"suggestions_count": 0,
|
| 426 |
+
"suggestions": [],
|
| 427 |
+
"span_issues": [],
|
| 428 |
+
"overcorrected": false,
|
| 429 |
+
"undercorrected": false,
|
| 430 |
+
"missed": [],
|
| 431 |
+
"elapsed_ms": 3689,
|
| 432 |
+
"original_returned": "أستخدم برنامج Python في العمل"
|
| 433 |
+
},
|
| 434 |
+
{
|
| 435 |
+
"input": "تعلمت JavaScript و CSS",
|
| 436 |
+
"category": "foreign",
|
| 437 |
+
"notes": "",
|
| 438 |
+
"status": "FAIL",
|
| 439 |
+
"suggestions_count": 2,
|
| 440 |
+
"suggestions": [
|
| 441 |
+
{
|
| 442 |
+
"type": "punctuation",
|
| 443 |
+
"original": "JavaScript",
|
| 444 |
+
"correction": "JavaScript:",
|
| 445 |
+
"start": 6,
|
| 446 |
+
"end": 16
|
| 447 |
+
},
|
| 448 |
+
{
|
| 449 |
+
"type": "spelling",
|
| 450 |
+
"original": "CSS",
|
| 451 |
+
"correction": "CS",
|
| 452 |
+
"start": 19,
|
| 453 |
+
"end": 22
|
| 454 |
+
}
|
| 455 |
+
],
|
| 456 |
+
"span_issues": [],
|
| 457 |
+
"overcorrected": true,
|
| 458 |
+
"undercorrected": false,
|
| 459 |
+
"missed": [],
|
| 460 |
+
"elapsed_ms": 2752,
|
| 461 |
+
"original_returned": "تعلمت JavaScript و CSS"
|
| 462 |
+
},
|
| 463 |
+
{
|
| 464 |
+
"input": "ايش تبي تسوي اليوم",
|
| 465 |
+
"category": "dialect",
|
| 466 |
+
"notes": "Gulf dialect",
|
| 467 |
+
"status": "OK",
|
| 468 |
+
"suggestions_count": 1,
|
| 469 |
+
"suggestions": [
|
| 470 |
+
{
|
| 471 |
+
"type": "punctuation",
|
| 472 |
+
"original": "اليوم",
|
| 473 |
+
"correction": "اليوم؟",
|
| 474 |
+
"start": 13,
|
| 475 |
+
"end": 18
|
| 476 |
+
}
|
| 477 |
+
],
|
| 478 |
+
"span_issues": [],
|
| 479 |
+
"overcorrected": false,
|
| 480 |
+
"undercorrected": false,
|
| 481 |
+
"missed": [],
|
| 482 |
+
"elapsed_ms": 2240,
|
| 483 |
+
"original_returned": "ايش تبي تسوي اليوم"
|
| 484 |
+
},
|
| 485 |
+
{
|
| 486 |
+
"input": "عايز اروح البيت",
|
| 487 |
+
"category": "dialect",
|
| 488 |
+
"notes": "Egyptian dialect",
|
| 489 |
+
"status": "FAIL",
|
| 490 |
+
"suggestions_count": 1,
|
| 491 |
+
"suggestions": [
|
| 492 |
+
{
|
| 493 |
+
"type": "punctuation",
|
| 494 |
+
"original": "البيت",
|
| 495 |
+
"correction": "البيت؟",
|
| 496 |
+
"start": 10,
|
| 497 |
+
"end": 15
|
| 498 |
+
}
|
| 499 |
+
],
|
| 500 |
+
"span_issues": [],
|
| 501 |
+
"overcorrected": false,
|
| 502 |
+
"undercorrected": true,
|
| 503 |
+
"missed": [
|
| 504 |
+
"عايز",
|
| 505 |
+
"اروح"
|
| 506 |
+
],
|
| 507 |
+
"elapsed_ms": 1895,
|
| 508 |
+
"original_returned": "عايز اروح البيت"
|
| 509 |
+
},
|
| 510 |
+
{
|
| 511 |
+
"input": "مررررحبا كيييييف حالك",
|
| 512 |
+
"category": "repetition",
|
| 513 |
+
"notes": "",
|
| 514 |
+
"status": "FAIL",
|
| 515 |
+
"suggestions_count": 1,
|
| 516 |
+
"suggestions": [
|
| 517 |
+
{
|
| 518 |
+
"type": "punctuation",
|
| 519 |
+
"original": "حالك",
|
| 520 |
+
"correction": "حالك.",
|
| 521 |
+
"start": 17,
|
| 522 |
+
"end": 21
|
| 523 |
+
}
|
| 524 |
+
],
|
| 525 |
+
"span_issues": [],
|
| 526 |
+
"overcorrected": false,
|
| 527 |
+
"undercorrected": true,
|
| 528 |
+
"missed": [
|
| 529 |
+
"مررررحبا",
|
| 530 |
+
"كيييييف"
|
| 531 |
+
],
|
| 532 |
+
"elapsed_ms": 2570,
|
| 533 |
+
"original_returned": "مررررحبا كيييييف حالك"
|
| 534 |
+
},
|
| 535 |
+
{
|
| 536 |
+
"input": "البنات ذهب إلى المدرسة",
|
| 537 |
+
"category": "grammar_SVA",
|
| 538 |
+
"notes": "",
|
| 539 |
+
"status": "FAIL",
|
| 540 |
+
"suggestions_count": 1,
|
| 541 |
+
"suggestions": [
|
| 542 |
+
{
|
| 543 |
+
"type": "punctuation",
|
| 544 |
+
"original": "المدرسة",
|
| 545 |
+
"correction": "المدرسة.",
|
| 546 |
+
"start": 15,
|
| 547 |
+
"end": 22
|
| 548 |
+
}
|
| 549 |
+
],
|
| 550 |
+
"span_issues": [],
|
| 551 |
+
"overcorrected": false,
|
| 552 |
+
"undercorrected": true,
|
| 553 |
+
"missed": [
|
| 554 |
+
"ذهب"
|
| 555 |
+
],
|
| 556 |
+
"elapsed_ms": 2400,
|
| 557 |
+
"original_returned": "البنات ذهب إلى المدرسة"
|
| 558 |
+
},
|
| 559 |
+
{
|
| 560 |
+
"input": "الطلاب يذهب إلى الجامعة",
|
| 561 |
+
"category": "grammar_SVA",
|
| 562 |
+
"notes": "",
|
| 563 |
+
"status": "OK",
|
| 564 |
+
"suggestions_count": 2,
|
| 565 |
+
"suggestions": [
|
| 566 |
+
{
|
| 567 |
+
"type": "grammar",
|
| 568 |
+
"original": "يذهب",
|
| 569 |
+
"correction": "يذهبون",
|
| 570 |
+
"start": 7,
|
| 571 |
+
"end": 11
|
| 572 |
+
},
|
| 573 |
+
{
|
| 574 |
+
"type": "punctuation",
|
| 575 |
+
"original": "الجامعة",
|
| 576 |
+
"correction": "الجامعة.",
|
| 577 |
+
"start": 16,
|
| 578 |
+
"end": 23
|
| 579 |
+
}
|
| 580 |
+
],
|
| 581 |
+
"span_issues": [],
|
| 582 |
+
"overcorrected": false,
|
| 583 |
+
"undercorrected": false,
|
| 584 |
+
"missed": [],
|
| 585 |
+
"elapsed_ms": 2604,
|
| 586 |
+
"original_returned": "الطلاب يذهب إلى الجامعة"
|
| 587 |
+
},
|
| 588 |
+
{
|
| 589 |
+
"input": "الأولاد لعب في الحديقة",
|
| 590 |
+
"category": "grammar_SVA",
|
| 591 |
+
"notes": "",
|
| 592 |
+
"status": "FAIL",
|
| 593 |
+
"suggestions_count": 1,
|
| 594 |
+
"suggestions": [
|
| 595 |
+
{
|
| 596 |
+
"type": "punctuation",
|
| 597 |
+
"original": "الحديقة",
|
| 598 |
+
"correction": "الحديقة.",
|
| 599 |
+
"start": 15,
|
| 600 |
+
"end": 22
|
| 601 |
+
}
|
| 602 |
+
],
|
| 603 |
+
"span_issues": [],
|
| 604 |
+
"overcorrected": false,
|
| 605 |
+
"undercorrected": true,
|
| 606 |
+
"missed": [
|
| 607 |
+
"لعب"
|
| 608 |
+
],
|
| 609 |
+
"elapsed_ms": 2340,
|
| 610 |
+
"original_returned": "الأولاد لعب في الحديقة"
|
| 611 |
+
},
|
| 612 |
+
{
|
| 613 |
+
"input": "هي ذهب إلى ا��سوق",
|
| 614 |
+
"category": "grammar_gender",
|
| 615 |
+
"notes": "",
|
| 616 |
+
"status": "OK",
|
| 617 |
+
"suggestions_count": 2,
|
| 618 |
+
"suggestions": [
|
| 619 |
+
{
|
| 620 |
+
"type": "grammar",
|
| 621 |
+
"original": "ذهب",
|
| 622 |
+
"correction": "ذهبت",
|
| 623 |
+
"start": 3,
|
| 624 |
+
"end": 6
|
| 625 |
+
},
|
| 626 |
+
{
|
| 627 |
+
"type": "punctuation",
|
| 628 |
+
"original": "السوق",
|
| 629 |
+
"correction": "السوق.",
|
| 630 |
+
"start": 11,
|
| 631 |
+
"end": 16
|
| 632 |
+
}
|
| 633 |
+
],
|
| 634 |
+
"span_issues": [],
|
| 635 |
+
"overcorrected": false,
|
| 636 |
+
"undercorrected": false,
|
| 637 |
+
"missed": [],
|
| 638 |
+
"elapsed_ms": 2290,
|
| 639 |
+
"original_returned": "هي ذهب إلى السوق"
|
| 640 |
+
},
|
| 641 |
+
{
|
| 642 |
+
"input": "الطالبة نجح في الامتحان",
|
| 643 |
+
"category": "grammar_gender",
|
| 644 |
+
"notes": "",
|
| 645 |
+
"status": "OK",
|
| 646 |
+
"suggestions_count": 2,
|
| 647 |
+
"suggestions": [
|
| 648 |
+
{
|
| 649 |
+
"type": "grammar",
|
| 650 |
+
"original": "نجح",
|
| 651 |
+
"correction": "نجحت",
|
| 652 |
+
"start": 8,
|
| 653 |
+
"end": 11
|
| 654 |
+
},
|
| 655 |
+
{
|
| 656 |
+
"type": "punctuation",
|
| 657 |
+
"original": "الامتحان",
|
| 658 |
+
"correction": "الامتحين.",
|
| 659 |
+
"start": 15,
|
| 660 |
+
"end": 23
|
| 661 |
+
}
|
| 662 |
+
],
|
| 663 |
+
"span_issues": [],
|
| 664 |
+
"overcorrected": false,
|
| 665 |
+
"undercorrected": false,
|
| 666 |
+
"missed": [],
|
| 667 |
+
"elapsed_ms": 2076,
|
| 668 |
+
"original_returned": "الطالبة نجح في الامتحان"
|
| 669 |
+
},
|
| 670 |
+
{
|
| 671 |
+
"input": "الطالب ذهب إلى المدرسة",
|
| 672 |
+
"category": "grammar_correct",
|
| 673 |
+
"notes": "",
|
| 674 |
+
"status": "FAIL",
|
| 675 |
+
"suggestions_count": 1,
|
| 676 |
+
"suggestions": [
|
| 677 |
+
{
|
| 678 |
+
"type": "punctuation",
|
| 679 |
+
"original": "المدرسة",
|
| 680 |
+
"correction": "المدرسة.",
|
| 681 |
+
"start": 15,
|
| 682 |
+
"end": 22
|
| 683 |
+
}
|
| 684 |
+
],
|
| 685 |
+
"span_issues": [],
|
| 686 |
+
"overcorrected": true,
|
| 687 |
+
"undercorrected": false,
|
| 688 |
+
"missed": [],
|
| 689 |
+
"elapsed_ms": 2190,
|
| 690 |
+
"original_returned": "الطالب ذهب إلى المدرسة"
|
| 691 |
+
},
|
| 692 |
+
{
|
| 693 |
+
"input": "الطالبة ذهبت إلى المدرسة",
|
| 694 |
+
"category": "grammar_correct",
|
| 695 |
+
"notes": "",
|
| 696 |
+
"status": "FAIL",
|
| 697 |
+
"suggestions_count": 1,
|
| 698 |
+
"suggestions": [
|
| 699 |
+
{
|
| 700 |
+
"type": "punctuation",
|
| 701 |
+
"original": "المدرسة",
|
| 702 |
+
"correction": "المدرسة.",
|
| 703 |
+
"start": 17,
|
| 704 |
+
"end": 24
|
| 705 |
+
}
|
| 706 |
+
],
|
| 707 |
+
"span_issues": [],
|
| 708 |
+
"overcorrected": true,
|
| 709 |
+
"undercorrected": false,
|
| 710 |
+
"missed": [],
|
| 711 |
+
"elapsed_ms": 2287,
|
| 712 |
+
"original_returned": "الطالبة ذهبت إلى المدرسة"
|
| 713 |
+
},
|
| 714 |
+
{
|
| 715 |
+
"input": "الطلاب ذهبوا إلى المدرسة",
|
| 716 |
+
"category": "grammar_correct",
|
| 717 |
+
"notes": "",
|
| 718 |
+
"status": "FAIL",
|
| 719 |
+
"suggestions_count": 1,
|
| 720 |
+
"suggestions": [
|
| 721 |
+
{
|
| 722 |
+
"type": "punctuation",
|
| 723 |
+
"original": "المدرسة",
|
| 724 |
+
"correction": "المدرسة.",
|
| 725 |
+
"start": 17,
|
| 726 |
+
"end": 24
|
| 727 |
+
}
|
| 728 |
+
],
|
| 729 |
+
"span_issues": [],
|
| 730 |
+
"overcorrected": true,
|
| 731 |
+
"undercorrected": false,
|
| 732 |
+
"missed": [],
|
| 733 |
+
"elapsed_ms": 2220,
|
| 734 |
+
"original_returned": "الطلاب ذهبوا إلى المدرسة"
|
| 735 |
+
},
|
| 736 |
+
{
|
| 737 |
+
"input": "هي ذهبت إلى السوق",
|
| 738 |
+
"category": "grammar_correct",
|
| 739 |
+
"notes": "",
|
| 740 |
+
"status": "FAIL",
|
| 741 |
+
"suggestions_count": 1,
|
| 742 |
+
"suggestions": [
|
| 743 |
+
{
|
| 744 |
+
"type": "punctuation",
|
| 745 |
+
"original": "السوق",
|
| 746 |
+
"correction": "السوق.",
|
| 747 |
+
"start": 12,
|
| 748 |
+
"end": 17
|
| 749 |
+
}
|
| 750 |
+
],
|
| 751 |
+
"span_issues": [],
|
| 752 |
+
"overcorrected": true,
|
| 753 |
+
"undercorrected": false,
|
| 754 |
+
"missed": [],
|
| 755 |
+
"elapsed_ms": 2094,
|
| 756 |
+
"original_returned": "هي ذهبت إلى السوق"
|
| 757 |
+
},
|
| 758 |
+
{
|
| 759 |
+
"input": "الولد ذهبوا إلى المدرسة",
|
| 760 |
+
"category": "grammar_number",
|
| 761 |
+
"notes": "",
|
| 762 |
+
"status": "OK",
|
| 763 |
+
"suggestions_count": 2,
|
| 764 |
+
"suggestions": [
|
| 765 |
+
{
|
| 766 |
+
"type": "grammar",
|
| 767 |
+
"original": "ذهبوا",
|
| 768 |
+
"correction": "ذهب",
|
| 769 |
+
"start": 6,
|
| 770 |
+
"end": 11
|
| 771 |
+
},
|
| 772 |
+
{
|
| 773 |
+
"type": "punctuation",
|
| 774 |
+
"original": "المدرسة",
|
| 775 |
+
"correction": "المدرسة.",
|
| 776 |
+
"start": 16,
|
| 777 |
+
"end": 23
|
| 778 |
+
}
|
| 779 |
+
],
|
| 780 |
+
"span_issues": [],
|
| 781 |
+
"overcorrected": false,
|
| 782 |
+
"undercorrected": false,
|
| 783 |
+
"missed": [],
|
| 784 |
+
"elapsed_ms": 2069,
|
| 785 |
+
"original_returned": "الولد ذهبوا إلى المدرسة"
|
| 786 |
+
},
|
| 787 |
+
{
|
| 788 |
+
"input": "كيف حالك انا بخير",
|
| 789 |
+
"category": "punct_missing",
|
| 790 |
+
"notes": "Should add ؟ and ، or .",
|
| 791 |
+
"status": "OK",
|
| 792 |
+
"suggestions_count": 1,
|
| 793 |
+
"suggestions": [
|
| 794 |
+
{
|
| 795 |
+
"type": "punctuation",
|
| 796 |
+
"original": "بخير",
|
| 797 |
+
"correction": "بخير؟",
|
| 798 |
+
"start": 13,
|
| 799 |
+
"end": 17
|
| 800 |
+
}
|
| 801 |
+
],
|
| 802 |
+
"span_issues": [],
|
| 803 |
+
"overcorrected": false,
|
| 804 |
+
"undercorrected": false,
|
| 805 |
+
"missed": [],
|
| 806 |
+
"elapsed_ms": 2189,
|
| 807 |
+
"original_returned": "كيف حالك انا بخير"
|
| 808 |
+
},
|
| 809 |
+
{
|
| 810 |
+
"input": "مرحبا كيف حالك اليوم",
|
| 811 |
+
"category": "punct_missing",
|
| 812 |
+
"notes": "",
|
| 813 |
+
"status": "OK",
|
| 814 |
+
"suggestions_count": 1,
|
| 815 |
+
"suggestions": [
|
| 816 |
+
{
|
| 817 |
+
"type": "punctuation",
|
| 818 |
+
"original": "اليوم",
|
| 819 |
+
"correction": "اليوم؟",
|
| 820 |
+
"start": 15,
|
| 821 |
+
"end": 20
|
| 822 |
+
}
|
| 823 |
+
],
|
| 824 |
+
"span_issues": [],
|
| 825 |
+
"overcorrected": false,
|
| 826 |
+
"undercorrected": false,
|
| 827 |
+
"missed": [],
|
| 828 |
+
"elapsed_ms": 2304,
|
| 829 |
+
"original_returned": "مرحبا كيف حالك اليوم"
|
| 830 |
+
},
|
| 831 |
+
{
|
| 832 |
+
"input": "هل أنت بخير نعم أنا بخير",
|
| 833 |
+
"category": "punct_missing",
|
| 834 |
+
"notes": "Should add ؟",
|
| 835 |
+
"status": "OK",
|
| 836 |
+
"suggestions_count": 1,
|
| 837 |
+
"suggestions": [
|
| 838 |
+
{
|
| 839 |
+
"type": "punctuation",
|
| 840 |
+
"original": "بخير",
|
| 841 |
+
"correction": "بخير.",
|
| 842 |
+
"start": 20,
|
| 843 |
+
"end": 24
|
| 844 |
+
}
|
| 845 |
+
],
|
| 846 |
+
"span_issues": [],
|
| 847 |
+
"overcorrected": false,
|
| 848 |
+
"undercorrected": false,
|
| 849 |
+
"missed": [],
|
| 850 |
+
"elapsed_ms": 2740,
|
| 851 |
+
"original_returned": "هل أنت بخير نعم أنا بخير"
|
| 852 |
+
},
|
| 853 |
+
{
|
| 854 |
+
"input": "كيف حالك؟ أنا بخير.",
|
| 855 |
+
"category": "punct_correct",
|
| 856 |
+
"notes": "",
|
| 857 |
+
"status": "FAIL",
|
| 858 |
+
"suggestions_count": 2,
|
| 859 |
+
"suggestions": [
|
| 860 |
+
{
|
| 861 |
+
"type": "grammar",
|
| 862 |
+
"original": "حالك؟",
|
| 863 |
+
"correction": "حالك ؟",
|
| 864 |
+
"start": 4,
|
| 865 |
+
"end": 9
|
| 866 |
+
},
|
| 867 |
+
{
|
| 868 |
+
"type": "grammar",
|
| 869 |
+
"original": "بخير.",
|
| 870 |
+
"correction": "بخير",
|
| 871 |
+
"start": 14,
|
| 872 |
+
"end": 19
|
| 873 |
+
}
|
| 874 |
+
],
|
| 875 |
+
"span_issues": [],
|
| 876 |
+
"overcorrected": true,
|
| 877 |
+
"undercorrected": false,
|
| 878 |
+
"missed": [],
|
| 879 |
+
"elapsed_ms": 2693,
|
| 880 |
+
"original_returned": "كيف حالك؟ أنا بخير."
|
| 881 |
+
},
|
| 882 |
+
{
|
| 883 |
+
"input": "مرحباً، كيف حالك؟",
|
| 884 |
+
"category": "punct_correct",
|
| 885 |
+
"notes": "",
|
| 886 |
+
"status": "FAIL",
|
| 887 |
+
"suggestions_count": 2,
|
| 888 |
+
"suggestions": [
|
| 889 |
+
{
|
| 890 |
+
"type": "grammar",
|
| 891 |
+
"original": "مرحباً،",
|
| 892 |
+
"correction": "مرحبا ،",
|
| 893 |
+
"start": 0,
|
| 894 |
+
"end": 7
|
| 895 |
+
},
|
| 896 |
+
{
|
| 897 |
+
"type": "grammar",
|
| 898 |
+
"original": "حالك؟",
|
| 899 |
+
"correction": "حالك ؟",
|
| 900 |
+
"start": 12,
|
| 901 |
+
"end": 17
|
| 902 |
+
}
|
| 903 |
+
],
|
| 904 |
+
"span_issues": [],
|
| 905 |
+
"overcorrected": true,
|
| 906 |
+
"undercorrected": false,
|
| 907 |
+
"missed": [],
|
| 908 |
+
"elapsed_ms": 2374,
|
| 909 |
+
"original_returned": "مرحباً، كيف حالك؟"
|
| 910 |
+
},
|
| 911 |
+
{
|
| 912 |
+
"input": "انا ذهب الى الجامعه كيف حالك",
|
| 913 |
+
"category": "pipeline_order",
|
| 914 |
+
"notes": "Should fix: spelling(انا→أنا, الى→إلى, الجامعه→الجامعة) + grammar(ذهب→ذهبت) + punct",
|
| 915 |
+
"status": "FAIL",
|
| 916 |
+
"suggestions_count": 4,
|
| 917 |
+
"suggestions": [
|
| 918 |
+
{
|
| 919 |
+
"type": "punctuation",
|
| 920 |
+
"original": "حالك",
|
| 921 |
+
"correction": "حالك؟",
|
| 922 |
+
"start": 24,
|
| 923 |
+
"end": 28
|
| 924 |
+
},
|
| 925 |
+
{
|
| 926 |
+
"type": "spelling",
|
| 927 |
+
"original": "انا",
|
| 928 |
+
"correction": "أنا",
|
| 929 |
+
"start": 0,
|
| 930 |
+
"end": 3
|
| 931 |
+
},
|
| 932 |
+
{
|
| 933 |
+
"type": "spelling",
|
| 934 |
+
"original": "الجامعه",
|
| 935 |
+
"correction": "الجامعة",
|
| 936 |
+
"start": 12,
|
| 937 |
+
"end": 19
|
| 938 |
+
},
|
| 939 |
+
{
|
| 940 |
+
"type": "spelling",
|
| 941 |
+
"original": "الى",
|
| 942 |
+
"correction": "إلى",
|
| 943 |
+
"start": 8,
|
| 944 |
+
"end": 11
|
| 945 |
+
}
|
| 946 |
+
],
|
| 947 |
+
"span_issues": [],
|
| 948 |
+
"overcorrected": false,
|
| 949 |
+
"undercorrected": true,
|
| 950 |
+
"missed": [
|
| 951 |
+
"ذهب"
|
| 952 |
+
],
|
| 953 |
+
"elapsed_ms": 2655,
|
| 954 |
+
"original_returned": "انا ذهب الى الجامعه كيف حالك"
|
| 955 |
+
},
|
| 956 |
+
{
|
| 957 |
+
"input": "أنا ذهبت إلى الجامعة.",
|
| 958 |
+
"category": "overcorrect_perfect",
|
| 959 |
+
"notes": "",
|
| 960 |
+
"status": "FAIL",
|
| 961 |
+
"suggestions_count": 1,
|
| 962 |
+
"suggestions": [
|
| 963 |
+
{
|
| 964 |
+
"type": "grammar",
|
| 965 |
+
"original": "الجامعة.",
|
| 966 |
+
"correction": "الجامعة",
|
| 967 |
+
"start": 13,
|
| 968 |
+
"end": 21
|
| 969 |
+
}
|
| 970 |
+
],
|
| 971 |
+
"span_issues": [],
|
| 972 |
+
"overcorrected": true,
|
| 973 |
+
"undercorrected": false,
|
| 974 |
+
"missed": [],
|
| 975 |
+
"elapsed_ms": 2166,
|
| 976 |
+
"original_returned": "أنا ذهبت إلى الجامعة."
|
| 977 |
+
},
|
| 978 |
+
{
|
| 979 |
+
"input": "الحمد لله رب العالمين.",
|
| 980 |
+
"category": "overcorrect_quran",
|
| 981 |
+
"notes": "",
|
| 982 |
+
"status": "FAIL",
|
| 983 |
+
"suggestions_count": 1,
|
| 984 |
+
"suggestions": [
|
| 985 |
+
{
|
| 986 |
+
"type": "grammar",
|
| 987 |
+
"original": "العالمين.",
|
| 988 |
+
"correction": "العالمين",
|
| 989 |
+
"start": 13,
|
| 990 |
+
"end": 22
|
| 991 |
+
}
|
| 992 |
+
],
|
| 993 |
+
"span_issues": [],
|
| 994 |
+
"overcorrected": true,
|
| 995 |
+
"undercorrected": false,
|
| 996 |
+
"missed": [],
|
| 997 |
+
"elapsed_ms": 2238,
|
| 998 |
+
"original_returned": "الحمد لله رب العالمين."
|
| 999 |
+
},
|
| 1000 |
+
{
|
| 1001 |
+
"input": "بسم الله الرحمن الرحيم.",
|
| 1002 |
+
"category": "overcorrect_quran",
|
| 1003 |
+
"notes": "",
|
| 1004 |
+
"status": "FAIL",
|
| 1005 |
+
"suggestions_count": 1,
|
| 1006 |
+
"suggestions": [
|
| 1007 |
+
{
|
| 1008 |
+
"type": "grammar",
|
| 1009 |
+
"original": "الرحيم.",
|
| 1010 |
+
"correction": "الرحيم",
|
| 1011 |
+
"start": 16,
|
| 1012 |
+
"end": 23
|
| 1013 |
+
}
|
| 1014 |
+
],
|
| 1015 |
+
"span_issues": [],
|
| 1016 |
+
"overcorrected": true,
|
| 1017 |
+
"undercorrected": false,
|
| 1018 |
+
"missed": [],
|
| 1019 |
+
"elapsed_ms": 2584,
|
| 1020 |
+
"original_returned": "بسم الله الرحمن الرحيم."
|
| 1021 |
+
},
|
| 1022 |
+
{
|
| 1023 |
+
"input": "إن الله مع الصابرين.",
|
| 1024 |
+
"category": "overcorrect_quran",
|
| 1025 |
+
"notes": "",
|
| 1026 |
+
"status": "FAIL",
|
| 1027 |
+
"suggestions_count": 1,
|
| 1028 |
+
"suggestions": [
|
| 1029 |
+
{
|
| 1030 |
+
"type": "grammar",
|
| 1031 |
+
"original": "الصابرين.",
|
| 1032 |
+
"correction": "الصابرين",
|
| 1033 |
+
"start": 11,
|
| 1034 |
+
"end": 20
|
| 1035 |
+
}
|
| 1036 |
+
],
|
| 1037 |
+
"span_issues": [],
|
| 1038 |
+
"overcorrected": true,
|
| 1039 |
+
"undercorrected": false,
|
| 1040 |
+
"missed": [],
|
| 1041 |
+
"elapsed_ms": 2206,
|
| 1042 |
+
"original_returned": "إن الله مع الصابرين."
|
| 1043 |
+
},
|
| 1044 |
+
{
|
| 1045 |
+
"input": "القاهرة عاصمة مصر.",
|
| 1046 |
+
"category": "overcorrect_fact",
|
| 1047 |
+
"notes": "",
|
| 1048 |
+
"status": "FAIL",
|
| 1049 |
+
"suggestions_count": 1,
|
| 1050 |
+
"suggestions": [
|
| 1051 |
+
{
|
| 1052 |
+
"type": "grammar",
|
| 1053 |
+
"original": "مصر.",
|
| 1054 |
+
"correction": "مصر",
|
| 1055 |
+
"start": 14,
|
| 1056 |
+
"end": 18
|
| 1057 |
+
}
|
| 1058 |
+
],
|
| 1059 |
+
"span_issues": [],
|
| 1060 |
+
"overcorrected": true,
|
| 1061 |
+
"undercorrected": false,
|
| 1062 |
+
"missed": [],
|
| 1063 |
+
"elapsed_ms": 2078,
|
| 1064 |
+
"original_returned": "القاهرة عاصمة مصر."
|
| 1065 |
+
},
|
| 1066 |
+
{
|
| 1067 |
+
"input": "الرياض عاصمة المملكة العربية السعودية.",
|
| 1068 |
+
"category": "overcorrect_fact",
|
| 1069 |
+
"notes": "",
|
| 1070 |
+
"status": "FAIL",
|
| 1071 |
+
"suggestions_count": 1,
|
| 1072 |
+
"suggestions": [
|
| 1073 |
+
{
|
| 1074 |
+
"type": "grammar",
|
| 1075 |
+
"original": "السعودية.",
|
| 1076 |
+
"correction": "السعودية",
|
| 1077 |
+
"start": 29,
|
| 1078 |
+
"end": 38
|
| 1079 |
+
}
|
| 1080 |
+
],
|
| 1081 |
+
"span_issues": [],
|
| 1082 |
+
"overcorrected": true,
|
| 1083 |
+
"undercorrected": false,
|
| 1084 |
+
"missed": [],
|
| 1085 |
+
"elapsed_ms": 2556,
|
| 1086 |
+
"original_returned": "الرياض عاصمة المملكة العربية السعودية."
|
| 1087 |
+
},
|
| 1088 |
+
{
|
| 1089 |
+
"input": "أحب القراءة والكتابة والرسم.",
|
| 1090 |
+
"category": "overcorrect_style",
|
| 1091 |
+
"notes": "",
|
| 1092 |
+
"status": "FAIL",
|
| 1093 |
+
"suggestions_count": 1,
|
| 1094 |
+
"suggestions": [
|
| 1095 |
+
{
|
| 1096 |
+
"type": "grammar",
|
| 1097 |
+
"original": "والرسم.",
|
| 1098 |
+
"correction": "والرسم",
|
| 1099 |
+
"start": 21,
|
| 1100 |
+
"end": 28
|
| 1101 |
+
}
|
| 1102 |
+
],
|
| 1103 |
+
"span_issues": [],
|
| 1104 |
+
"overcorrected": true,
|
| 1105 |
+
"undercorrected": false,
|
| 1106 |
+
"missed": [],
|
| 1107 |
+
"elapsed_ms": 2407,
|
| 1108 |
+
"original_returned": "أحب القراءة والكتابة والرسم."
|
| 1109 |
+
},
|
| 1110 |
+
{
|
| 1111 |
+
"input": "كان الجو جميلاً في ذلك اليوم.",
|
| 1112 |
+
"category": "overcorrect_style",
|
| 1113 |
+
"notes": "",
|
| 1114 |
+
"status": "FAIL",
|
| 1115 |
+
"suggestions_count": 2,
|
| 1116 |
+
"suggestions": [
|
| 1117 |
+
{
|
| 1118 |
+
"type": "grammar",
|
| 1119 |
+
"original": "اليوم.",
|
| 1120 |
+
"correction": "اليوم",
|
| 1121 |
+
"start": 23,
|
| 1122 |
+
"end": 29
|
| 1123 |
+
},
|
| 1124 |
+
{
|
| 1125 |
+
"type": "spelling",
|
| 1126 |
+
"original": "جميلاً",
|
| 1127 |
+
"correction": "جميلا",
|
| 1128 |
+
"start": 9,
|
| 1129 |
+
"end": 15
|
| 1130 |
+
}
|
| 1131 |
+
],
|
| 1132 |
+
"span_issues": [],
|
| 1133 |
+
"overcorrected": true,
|
| 1134 |
+
"undercorrected": false,
|
| 1135 |
+
"missed": [],
|
| 1136 |
+
"elapsed_ms": 2862,
|
| 1137 |
+
"original_returned": "كان الجو جميلاً في ذلك اليوم."
|
| 1138 |
+
},
|
| 1139 |
+
{
|
| 1140 |
+
"input": "والمدرسة قريبة من البيت",
|
| 1141 |
+
"category": "morph_prefix",
|
| 1142 |
+
"notes": "و+المدرسة correct",
|
| 1143 |
+
"status": "FAIL",
|
| 1144 |
+
"suggestions_count": 1,
|
| 1145 |
+
"suggestions": [
|
| 1146 |
+
{
|
| 1147 |
+
"type": "punctuation",
|
| 1148 |
+
"original": "البيت",
|
| 1149 |
+
"correction": "البيت.",
|
| 1150 |
+
"start": 18,
|
| 1151 |
+
"end": 23
|
| 1152 |
+
}
|
| 1153 |
+
],
|
| 1154 |
+
"span_issues": [],
|
| 1155 |
+
"overcorrected": true,
|
| 1156 |
+
"undercorrected": false,
|
| 1157 |
+
"missed": [],
|
| 1158 |
+
"elapsed_ms": 2406,
|
| 1159 |
+
"original_returned": "والمدرسة قريبة من البيت"
|
| 1160 |
+
},
|
| 1161 |
+
{
|
| 1162 |
+
"input": "فالكتاب على الطاولة",
|
| 1163 |
+
"category": "morph_prefix",
|
| 1164 |
+
"notes": "ف+الكتاب correct",
|
| 1165 |
+
"status": "FAIL",
|
| 1166 |
+
"suggestions_count": 1,
|
| 1167 |
+
"suggestions": [
|
| 1168 |
+
{
|
| 1169 |
+
"type": "punctuation",
|
| 1170 |
+
"original": "الطاولة",
|
| 1171 |
+
"correction": "الطاولة.",
|
| 1172 |
+
"start": 12,
|
| 1173 |
+
"end": 19
|
| 1174 |
+
}
|
| 1175 |
+
],
|
| 1176 |
+
"span_issues": [],
|
| 1177 |
+
"overcorrected": true,
|
| 1178 |
+
"undercorrected": false,
|
| 1179 |
+
"missed": [],
|
| 1180 |
+
"elapsed_ms": 2360,
|
| 1181 |
+
"original_returned": "فالكتاب على الطاولة"
|
| 1182 |
+
},
|
| 1183 |
+
{
|
| 1184 |
+
"input": "بالمدرسة يوجد مكتبة",
|
| 1185 |
+
"category": "morph_prefix",
|
| 1186 |
+
"notes": "ب+المدرسة correct",
|
| 1187 |
+
"status": "FAIL",
|
| 1188 |
+
"suggestions_count": 2,
|
| 1189 |
+
"suggestions": [
|
| 1190 |
+
{
|
| 1191 |
+
"type": "grammar",
|
| 1192 |
+
"original": "يوجد",
|
| 1193 |
+
"correction": "توجد",
|
| 1194 |
+
"start": 9,
|
| 1195 |
+
"end": 13
|
| 1196 |
+
},
|
| 1197 |
+
{
|
| 1198 |
+
"type": "punctuation",
|
| 1199 |
+
"original": "مكتبة",
|
| 1200 |
+
"correction": "مكتبة.",
|
| 1201 |
+
"start": 14,
|
| 1202 |
+
"end": 19
|
| 1203 |
+
}
|
| 1204 |
+
],
|
| 1205 |
+
"span_issues": [],
|
| 1206 |
+
"overcorrected": true,
|
| 1207 |
+
"undercorrected": false,
|
| 1208 |
+
"missed": [],
|
| 1209 |
+
"elapsed_ms": 2100,
|
| 1210 |
+
"original_returned": "بالمدرسة يوجد مكتبة"
|
| 1211 |
+
},
|
| 1212 |
+
{
|
| 1213 |
+
"input": "كالنار في الحطب",
|
| 1214 |
+
"category": "morph_prefix",
|
| 1215 |
+
"notes": "ك+النار correct",
|
| 1216 |
+
"status": "FAIL",
|
| 1217 |
+
"suggestions_count": 1,
|
| 1218 |
+
"suggestions": [
|
| 1219 |
+
{
|
| 1220 |
+
"type": "punctuation",
|
| 1221 |
+
"original": "الحطب",
|
| 1222 |
+
"correction": "الحطب.",
|
| 1223 |
+
"start": 10,
|
| 1224 |
+
"end": 15
|
| 1225 |
+
}
|
| 1226 |
+
],
|
| 1227 |
+
"span_issues": [],
|
| 1228 |
+
"overcorrected": true,
|
| 1229 |
+
"undercorrected": false,
|
| 1230 |
+
"missed": [],
|
| 1231 |
+
"elapsed_ms": 2206,
|
| 1232 |
+
"original_returned": "كالنار في الحطب"
|
| 1233 |
+
},
|
| 1234 |
+
{
|
| 1235 |
+
"input": "للطلاب حق في التعليم",
|
| 1236 |
+
"category": "morph_prefix",
|
| 1237 |
+
"notes": "ل+الطلاب correct",
|
| 1238 |
+
"status": "FAIL",
|
| 1239 |
+
"suggestions_count": 1,
|
| 1240 |
+
"suggestions": [
|
| 1241 |
+
{
|
| 1242 |
+
"type": "punctuation",
|
| 1243 |
+
"original": "التعليم",
|
| 1244 |
+
"correction": "التعليم.",
|
| 1245 |
+
"start": 13,
|
| 1246 |
+
"end": 20
|
| 1247 |
+
}
|
| 1248 |
+
],
|
| 1249 |
+
"span_issues": [],
|
| 1250 |
+
"overcorrected": true,
|
| 1251 |
+
"undercorrected": false,
|
| 1252 |
+
"missed": [],
|
| 1253 |
+
"elapsed_ms": 2364,
|
| 1254 |
+
"original_returned": "للطلاب حق في التعليم"
|
| 1255 |
+
},
|
| 1256 |
+
{
|
| 1257 |
+
"input": "",
|
| 1258 |
+
"category": "edge_empty",
|
| 1259 |
+
"notes": "Empty string",
|
| 1260 |
+
"status": "OK",
|
| 1261 |
+
"suggestions_count": 0,
|
| 1262 |
+
"suggestions": [],
|
| 1263 |
+
"span_issues": [],
|
| 1264 |
+
"overcorrected": false,
|
| 1265 |
+
"undercorrected": false,
|
| 1266 |
+
"missed": [],
|
| 1267 |
+
"elapsed_ms": 546,
|
| 1268 |
+
"original_returned": ""
|
| 1269 |
+
},
|
| 1270 |
+
{
|
| 1271 |
+
"input": " ",
|
| 1272 |
+
"category": "edge_whitespace",
|
| 1273 |
+
"notes": "Only whitespace",
|
| 1274 |
+
"status": "OK",
|
| 1275 |
+
"suggestions_count": 0,
|
| 1276 |
+
"suggestions": [],
|
| 1277 |
+
"span_issues": [],
|
| 1278 |
+
"overcorrected": false,
|
| 1279 |
+
"undercorrected": false,
|
| 1280 |
+
"missed": [],
|
| 1281 |
+
"elapsed_ms": 673,
|
| 1282 |
+
"original_returned": " "
|
| 1283 |
+
},
|
| 1284 |
+
{
|
| 1285 |
+
"input": "Hello World",
|
| 1286 |
+
"category": "edge_english",
|
| 1287 |
+
"notes": "English only — no correction",
|
| 1288 |
+
"status": "OK",
|
| 1289 |
+
"suggestions_count": 0,
|
| 1290 |
+
"suggestions": [],
|
| 1291 |
+
"span_issues": [],
|
| 1292 |
+
"overcorrected": false,
|
| 1293 |
+
"undercorrected": false,
|
| 1294 |
+
"missed": [],
|
| 1295 |
+
"elapsed_ms": 924,
|
| 1296 |
+
"original_returned": "Hello World"
|
| 1297 |
+
},
|
| 1298 |
+
{
|
| 1299 |
+
"input": "12345 67890",
|
| 1300 |
+
"category": "edge_numbers",
|
| 1301 |
+
"notes": "Numbers only",
|
| 1302 |
+
"status": "FAIL",
|
| 1303 |
+
"suggestions_count": 1,
|
| 1304 |
+
"suggestions": [
|
| 1305 |
+
{
|
| 1306 |
+
"type": "punctuation",
|
| 1307 |
+
"original": "67890",
|
| 1308 |
+
"correction": "67890.",
|
| 1309 |
+
"start": 6,
|
| 1310 |
+
"end": 11
|
| 1311 |
+
}
|
| 1312 |
+
],
|
| 1313 |
+
"span_issues": [],
|
| 1314 |
+
"overcorrected": true,
|
| 1315 |
+
"undercorrected": false,
|
| 1316 |
+
"missed": [],
|
| 1317 |
+
"elapsed_ms": 2844,
|
| 1318 |
+
"original_returned": "12345 67890"
|
| 1319 |
+
},
|
| 1320 |
+
{
|
| 1321 |
+
"input": "😀🎉👍",
|
| 1322 |
+
"category": "edge_emoji",
|
| 1323 |
+
"notes": "Emojis only",
|
| 1324 |
+
"status": "FAIL",
|
| 1325 |
+
"suggestions_count": 1,
|
| 1326 |
+
"suggestions": [
|
| 1327 |
+
{
|
| 1328 |
+
"type": "grammar",
|
| 1329 |
+
"original": "😀🎉👍",
|
| 1330 |
+
"correction": "😀 🎉 👍",
|
| 1331 |
+
"start": 0,
|
| 1332 |
+
"end": 3
|
| 1333 |
+
}
|
| 1334 |
+
],
|
| 1335 |
+
"span_issues": [],
|
| 1336 |
+
"overcorrected": true,
|
| 1337 |
+
"undercorrected": false,
|
| 1338 |
+
"missed": [],
|
| 1339 |
+
"elapsed_ms": 1923,
|
| 1340 |
+
"original_returned": "😀🎉👍"
|
| 1341 |
+
},
|
| 1342 |
+
{
|
| 1343 |
+
"input": "أنا 😊 سعيد اليوم 🎉",
|
| 1344 |
+
"category": "edge_mixed_emoji",
|
| 1345 |
+
"notes": "",
|
| 1346 |
+
"status": "OK",
|
| 1347 |
+
"suggestions_count": 0,
|
| 1348 |
+
"suggestions": [],
|
| 1349 |
+
"span_issues": [],
|
| 1350 |
+
"overcorrected": false,
|
| 1351 |
+
"undercorrected": false,
|
| 1352 |
+
"missed": [],
|
| 1353 |
+
"elapsed_ms": 2468,
|
| 1354 |
+
"original_returned": "أنا 😊 سعيد اليوم 🎉"
|
| 1355 |
+
},
|
| 1356 |
+
{
|
| 1357 |
+
"input": "https://www.example.com",
|
| 1358 |
+
"category": "edge_url",
|
| 1359 |
+
"notes": "URL should not be modified",
|
| 1360 |
+
"status": "OK",
|
| 1361 |
+
"suggestions_count": 0,
|
| 1362 |
+
"suggestions": [],
|
| 1363 |
+
"span_issues": [],
|
| 1364 |
+
"overcorrected": false,
|
| 1365 |
+
"undercorrected": false,
|
| 1366 |
+
"missed": [],
|
| 1367 |
+
"elapsed_ms": 584,
|
| 1368 |
+
"original_returned": "https://www.example.com"
|
| 1369 |
+
},
|
| 1370 |
+
{
|
| 1371 |
+
"input": "user@email.com",
|
| 1372 |
+
"category": "edge_email",
|
| 1373 |
+
"notes": "Email should not be modified",
|
| 1374 |
+
"status": "OK",
|
| 1375 |
+
"suggestions_count": 0,
|
| 1376 |
+
"suggestions": [],
|
| 1377 |
+
"span_issues": [],
|
| 1378 |
+
"overcorrected": false,
|
| 1379 |
+
"undercorrected": false,
|
| 1380 |
+
"missed": [],
|
| 1381 |
+
"elapsed_ms": 620,
|
| 1382 |
+
"original_returned": "user@email.com"
|
| 1383 |
+
},
|
| 1384 |
+
{
|
| 1385 |
+
"input": "٢٠٢٤/٠١/١٥",
|
| 1386 |
+
"category": "edge_arabic_numbers",
|
| 1387 |
+
"notes": "",
|
| 1388 |
+
"status": "FAIL",
|
| 1389 |
+
"suggestions_count": 1,
|
| 1390 |
+
"suggestions": [
|
| 1391 |
+
{
|
| 1392 |
+
"type": "grammar",
|
| 1393 |
+
"original": "٢٠٢٤/٠١/١٥",
|
| 1394 |
+
"correction": "٢٠٢٤ / ٠١ / ١٥",
|
| 1395 |
+
"start": 0,
|
| 1396 |
+
"end": 10
|
| 1397 |
+
}
|
| 1398 |
+
],
|
| 1399 |
+
"span_issues": [],
|
| 1400 |
+
"overcorrected": true,
|
| 1401 |
+
"undercorrected": false,
|
| 1402 |
+
"missed": [],
|
| 1403 |
+
"elapsed_ms": 3989,
|
| 1404 |
+
"original_returned": "٢٠٢٤/٠١/١٥"
|
| 1405 |
+
},
|
| 1406 |
+
{
|
| 1407 |
+
"input": "أستخدم Google Chrome للتصفح",
|
| 1408 |
+
"category": "mixed_ar_en",
|
| 1409 |
+
"notes": "",
|
| 1410 |
+
"status": "FAIL",
|
| 1411 |
+
"suggestions_count": 2,
|
| 1412 |
+
"suggestions": [
|
| 1413 |
+
{
|
| 1414 |
+
"type": "grammar",
|
| 1415 |
+
"original": "أستخدم",
|
| 1416 |
+
"correction": "أستخدمت",
|
| 1417 |
+
"start": 0,
|
| 1418 |
+
"end": 6
|
| 1419 |
+
},
|
| 1420 |
+
{
|
| 1421 |
+
"type": "punctuation",
|
| 1422 |
+
"original": "للتصفح",
|
| 1423 |
+
"correction": "للتصفح؟",
|
| 1424 |
+
"start": 21,
|
| 1425 |
+
"end": 27
|
| 1426 |
+
}
|
| 1427 |
+
],
|
| 1428 |
+
"span_issues": [],
|
| 1429 |
+
"overcorrected": true,
|
| 1430 |
+
"undercorrected": false,
|
| 1431 |
+
"missed": [],
|
| 1432 |
+
"elapsed_ms": 3047,
|
| 1433 |
+
"original_returned": "أستخدم Google Chrome للتصفح"
|
| 1434 |
+
},
|
| 1435 |
+
{
|
| 1436 |
+
"input": "درست في MIT ثم عملت في Google",
|
| 1437 |
+
"category": "mixed_ar_en",
|
| 1438 |
+
"notes": "",
|
| 1439 |
+
"status": "FAIL",
|
| 1440 |
+
"suggestions_count": 1,
|
| 1441 |
+
"suggestions": [
|
| 1442 |
+
{
|
| 1443 |
+
"type": "punctuation",
|
| 1444 |
+
"original": "Google",
|
| 1445 |
+
"correction": "Google.",
|
| 1446 |
+
"start": 23,
|
| 1447 |
+
"end": 29
|
| 1448 |
+
}
|
| 1449 |
+
],
|
| 1450 |
+
"span_issues": [],
|
| 1451 |
+
"overcorrected": true,
|
| 1452 |
+
"undercorrected": false,
|
| 1453 |
+
"missed": [],
|
| 1454 |
+
"elapsed_ms": 3260,
|
| 1455 |
+
"original_returned": "درست في MIT ثم عملت في Google"
|
| 1456 |
+
},
|
| 1457 |
+
{
|
| 1458 |
+
"input": "الـ API تعمل بشكل جيد",
|
| 1459 |
+
"category": "mixed_tech",
|
| 1460 |
+
"notes": "",
|
| 1461 |
+
"status": "FAIL",
|
| 1462 |
+
"suggestions_count": 2,
|
| 1463 |
+
"suggestions": [
|
| 1464 |
+
{
|
| 1465 |
+
"type": "punctuation",
|
| 1466 |
+
"original": "جيد",
|
| 1467 |
+
"correction": "جيد.",
|
| 1468 |
+
"start": 18,
|
| 1469 |
+
"end": 21
|
| 1470 |
+
},
|
| 1471 |
+
{
|
| 1472 |
+
"type": "spelling",
|
| 1473 |
+
"original": "الـ",
|
| 1474 |
+
"correction": "ال",
|
| 1475 |
+
"start": 0,
|
| 1476 |
+
"end": 3
|
| 1477 |
+
}
|
| 1478 |
+
],
|
| 1479 |
+
"span_issues": [],
|
| 1480 |
+
"overcorrected": true,
|
| 1481 |
+
"undercorrected": false,
|
| 1482 |
+
"missed": [],
|
| 1483 |
+
"elapsed_ms": 2691,
|
| 1484 |
+
"original_returned": "الـ API تعمل بشكل جيد"
|
| 1485 |
+
},
|
| 1486 |
+
{
|
| 1487 |
+
"input": "إن التعليم هو أساس التقدم والازدهار في المجتمعات.",
|
| 1488 |
+
"category": "academic_correct",
|
| 1489 |
+
"notes": "",
|
| 1490 |
+
"status": "FAIL",
|
| 1491 |
+
"suggestions_count": 1,
|
| 1492 |
+
"suggestions": [
|
| 1493 |
+
{
|
| 1494 |
+
"type": "grammar",
|
| 1495 |
+
"original": "المجتمعات.",
|
| 1496 |
+
"correction": "المجتمعات",
|
| 1497 |
+
"start": 39,
|
| 1498 |
+
"end": 49
|
| 1499 |
+
}
|
| 1500 |
+
],
|
| 1501 |
+
"span_issues": [],
|
| 1502 |
+
"overcorrected": true,
|
| 1503 |
+
"undercorrected": false,
|
| 1504 |
+
"missed": [],
|
| 1505 |
+
"elapsed_ms": 3085,
|
| 1506 |
+
"original_returned": "إن التعليم هو أساس التقدم والازدهار في المجتمعات."
|
| 1507 |
+
},
|
| 1508 |
+
{
|
| 1509 |
+
"input": "يعتبر البحث العلمي ركيزة أساسية في تطوير الأمم.",
|
| 1510 |
+
"category": "academic_correct",
|
| 1511 |
+
"notes": "",
|
| 1512 |
+
"status": "FAIL",
|
| 1513 |
+
"suggestions_count": 1,
|
| 1514 |
+
"suggestions": [
|
| 1515 |
+
{
|
| 1516 |
+
"type": "grammar",
|
| 1517 |
+
"original": "الأمم.",
|
| 1518 |
+
"correction": "الأمم",
|
| 1519 |
+
"start": 41,
|
| 1520 |
+
"end": 47
|
| 1521 |
+
}
|
| 1522 |
+
],
|
| 1523 |
+
"span_issues": [],
|
| 1524 |
+
"overcorrected": true,
|
| 1525 |
+
"undercorrected": false,
|
| 1526 |
+
"missed": [],
|
| 1527 |
+
"elapsed_ms": 4030,
|
| 1528 |
+
"original_returned": "يعتبر البحث العلمي ركيزة أساسية في تطوير الأمم."
|
| 1529 |
+
},
|
| 1530 |
+
{
|
| 1531 |
+
"input": "يااااا سلام عليك",
|
| 1532 |
+
"category": "social_repetition",
|
| 1533 |
+
"notes": "",
|
| 1534 |
+
"status": "OK",
|
| 1535 |
+
"suggestions_count": 2,
|
| 1536 |
+
"suggestions": [
|
| 1537 |
+
{
|
| 1538 |
+
"type": "grammar",
|
| 1539 |
+
"original": "يااااا",
|
| 1540 |
+
"correction": "يا",
|
| 1541 |
+
"start": 0,
|
| 1542 |
+
"end": 6
|
| 1543 |
+
},
|
| 1544 |
+
{
|
| 1545 |
+
"type": "punctuation",
|
| 1546 |
+
"original": "عليك",
|
| 1547 |
+
"correction": "عليك.",
|
| 1548 |
+
"start": 12,
|
| 1549 |
+
"end": 16
|
| 1550 |
+
}
|
| 1551 |
+
],
|
| 1552 |
+
"span_issues": [],
|
| 1553 |
+
"overcorrected": false,
|
| 1554 |
+
"undercorrected": false,
|
| 1555 |
+
"missed": [],
|
| 1556 |
+
"elapsed_ms": 2517,
|
| 1557 |
+
"original_returned": "يااااا سلام عليك"
|
| 1558 |
+
},
|
| 1559 |
+
{
|
| 1560 |
+
"input": "هههههههه مضحك جدا",
|
| 1561 |
+
"category": "social_laughter",
|
| 1562 |
+
"notes": "",
|
| 1563 |
+
"status": "OK",
|
| 1564 |
+
"suggestions_count": 2,
|
| 1565 |
+
"suggestions": [
|
| 1566 |
+
{
|
| 1567 |
+
"type": "grammar",
|
| 1568 |
+
"original": "هههههههه",
|
| 1569 |
+
"correction": "هههههاههه",
|
| 1570 |
+
"start": 0,
|
| 1571 |
+
"end": 8
|
| 1572 |
+
},
|
| 1573 |
+
{
|
| 1574 |
+
"type": "punctuation",
|
| 1575 |
+
"original": "جدا",
|
| 1576 |
+
"correction": "جدا.",
|
| 1577 |
+
"start": 14,
|
| 1578 |
+
"end": 17
|
| 1579 |
+
}
|
| 1580 |
+
],
|
| 1581 |
+
"span_issues": [],
|
| 1582 |
+
"overcorrected": false,
|
| 1583 |
+
"undercorrected": false,
|
| 1584 |
+
"missed": [],
|
| 1585 |
+
"elapsed_ms": 2645,
|
| 1586 |
+
"original_returned": "هههههههه مضحك جدا"
|
| 1587 |
+
},
|
| 1588 |
+
{
|
| 1589 |
+
"input": "واللللللله ما اعرف",
|
| 1590 |
+
"category": "social_emphasis",
|
| 1591 |
+
"notes": "",
|
| 1592 |
+
"status": "OK",
|
| 1593 |
+
"suggestions_count": 2,
|
| 1594 |
+
"suggestions": [
|
| 1595 |
+
{
|
| 1596 |
+
"type": "grammar",
|
| 1597 |
+
"original": "واللللللله ما",
|
| 1598 |
+
"correction": "والله لا",
|
| 1599 |
+
"start": 0,
|
| 1600 |
+
"end": 13
|
| 1601 |
+
},
|
| 1602 |
+
{
|
| 1603 |
+
"type": "punctuation",
|
| 1604 |
+
"original": "اعرف",
|
| 1605 |
+
"correction": "أعرف.",
|
| 1606 |
+
"start": 14,
|
| 1607 |
+
"end": 18
|
| 1608 |
+
}
|
| 1609 |
+
],
|
| 1610 |
+
"span_issues": [],
|
| 1611 |
+
"overcorrected": false,
|
| 1612 |
+
"undercorrected": false,
|
| 1613 |
+
"missed": [],
|
| 1614 |
+
"elapsed_ms": 1809,
|
| 1615 |
+
"original_returned": "واللللللله ما اعرف"
|
| 1616 |
+
},
|
| 1617 |
+
{
|
| 1618 |
+
"input": "انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا. انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا. انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا. انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا. انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا. انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا. انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا. انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا. انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا. انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا. انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا. انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا. انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا. انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا. انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا. انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا. انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا. انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا. انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا. انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا. انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا. انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا. انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا. انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا. انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا. ",
|
| 1619 |
+
"category": "stress_500w",
|
| 1620 |
+
"notes": "~500 words repeated errors",
|
| 1621 |
+
"status": "OK",
|
| 1622 |
+
"suggestions_count": 10,
|
| 1623 |
+
"suggestions": [
|
| 1624 |
+
{
|
| 1625 |
+
"type": "grammar",
|
| 1626 |
+
"original": "المدرسه",
|
| 1627 |
+
"correction": "المدرسة اليوم كان الجو جميل ذ ذ ، انا اذهب إلى المدرسةاليوم وكان الجو جميلة جدا ، فأنا ذهبت إلى للمدرسة",
|
| 1628 |
+
"start": 60,
|
| 1629 |
+
"end": 67
|
| 1630 |
+
},
|
| 1631 |
+
{
|
| 1632 |
+
"type": "grammar",
|
| 1633 |
+
"original": "جدا.",
|
| 1634 |
+
"correction": "جدا من",
|
| 1635 |
+
"start": 89,
|
| 1636 |
+
"end": 93
|
| 1637 |
+
},
|
| 1638 |
+
{
|
| 1639 |
+
"type": "grammar",
|
| 1640 |
+
"original": "الي المدرسه",
|
| 1641 |
+
"correction": "إلي المدرسة اليوموكان الجو جميلَ جدا ، وانا ذهبت إلى مدرسته",
|
| 1642 |
+
"start": 103,
|
| 1643 |
+
"end": 114
|
| 1644 |
+
},
|
| 1645 |
+
{
|
| 1646 |
+
"type": "grammar",
|
| 1647 |
+
"original": "الجو",
|
| 1648 |
+
"correction": "الهدوء",
|
| 1649 |
+
"start": 126,
|
| 1650 |
+
"end": 130
|
| 1651 |
+
},
|
| 1652 |
+
{
|
| 1653 |
+
"type": "grammar",
|
| 1654 |
+
"original": "الي المدرسه",
|
| 1655 |
+
"correction": "الذهاب إلى المدرسة واليوم وكان الجو رائع جدا ، انت ذهبت إلى بالمدرسة",
|
| 1656 |
+
"start": 150,
|
| 1657 |
+
"end": 161
|
| 1658 |
+
},
|
| 1659 |
+
{
|
| 1660 |
+
"type": "grammar",
|
| 1661 |
+
"original": "الجو",
|
| 1662 |
+
"correction": "الجوي",
|
| 1663 |
+
"start": 173,
|
| 1664 |
+
"end": 177
|
| 1665 |
+
},
|
| 1666 |
+
{
|
| 1667 |
+
"type": "punctuation",
|
| 1668 |
+
"original": "ل",
|
| 1669 |
+
"correction": "وكانت،",
|
| 1670 |
+
"start": 22,
|
| 1671 |
+
"end": 23
|
| 1672 |
+
},
|
| 1673 |
+
{
|
| 1674 |
+
"type": "punctuation",
|
| 1675 |
+
"original": "م",
|
| 1676 |
+
"correction": "جدّا.",
|
| 1677 |
+
"start": 25,
|
| 1678 |
+
"end": 26
|
| 1679 |
+
},
|
| 1680 |
+
{
|
| 1681 |
+
"type": "punctuation",
|
| 1682 |
+
"original": "ج",
|
| 1683 |
+
"correction": "جدا.،",
|
| 1684 |
+
"start": 34,
|
| 1685 |
+
"end": 35
|
| 1686 |
+
},
|
| 1687 |
+
{
|
| 1688 |
+
"type": "spelling",
|
| 1689 |
+
"original": "الي المدرسه",
|
| 1690 |
+
"correction": "إلى المدرسة",
|
| 1691 |
+
"start": 9,
|
| 1692 |
+
"end": 20
|
| 1693 |
+
}
|
| 1694 |
+
],
|
| 1695 |
+
"span_issues": [],
|
| 1696 |
+
"overcorrected": false,
|
| 1697 |
+
"undercorrected": false,
|
| 1698 |
+
"missed": [],
|
| 1699 |
+
"elapsed_ms": 24075,
|
| 1700 |
+
"original_returned": "انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا. انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا. انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا. انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا. انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا. انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا. انا ذهبت الي المدرسه اليوم وكان الجو جميل ج��ا. انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا. انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا. انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا. انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا. انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا. انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا. انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا. انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا. انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا. انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا. انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا. انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا. انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا. انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا. انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا. انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا. انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا. انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا."
|
| 1701 |
+
}
|
| 1702 |
+
]
|
src/app.py
CHANGED
|
@@ -819,6 +819,8 @@ def _is_small_spelling_change(orig_word, corr_word, vocab_manager=None):
|
|
| 819 |
# Verb/particle confusion: كان (was) ↔ كأن (as if) = ALWAYS wrong
|
| 820 |
'كان': {'كأن'},
|
| 821 |
'كأن': {'كان'},
|
|
|
|
|
|
|
| 822 |
# Preposition confusion: different meanings, both valid
|
| 823 |
'إلى': {'على', 'علي'},
|
| 824 |
'على': {'إلى', 'علي'},
|
|
@@ -1285,6 +1287,59 @@ def analyze_text():
|
|
| 1285 |
if not run_spelling:
|
| 1286 |
logger.info(f"[ANALYZE] Text length {text_len} > 300 — skipping AraSpell for performance")
|
| 1287 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1288 |
# 1. Spelling (with conservative post-filtering to avoid over-editing)
|
| 1289 |
if run_spelling:
|
| 1290 |
try:
|
|
@@ -1468,21 +1523,8 @@ def analyze_text():
|
|
| 1468 |
logger.error(traceback.format_exc())
|
| 1469 |
timing_ms['spelling_error'] = f"{type(e).__name__}: {str(e)[:200]}"
|
| 1470 |
|
| 1471 |
-
# ── FIX-07: Religious text
|
| 1472 |
-
|
| 1473 |
-
'بسم الله', 'الحمد لله', 'سبحان الله', 'لا إله إلا الله',
|
| 1474 |
-
'إياك نعبد', 'قل هو الله', 'قل أعوذ', 'إنا أنزلناه',
|
| 1475 |
-
'حسبنا الله', 'لا حول ولا قوة', 'أستغفر الله',
|
| 1476 |
-
'الله أكبر', 'إنا لله', 'اللهم صل', 'وإياك نستعي��',
|
| 1477 |
-
'ذلك الكتاب لا ريب', 'مالك يوم الدين', 'لم يلد ولم يولد',
|
| 1478 |
-
'الله لا إله إلا هو', 'الرحمن الرحيم', 'رب العالمين',
|
| 1479 |
-
'إنما الأعمال بالنيات', 'السلام عليكم ورحمة الله',
|
| 1480 |
-
'صراط الذين أنعمت', 'من شر ما خلق', 'ملك الناس',
|
| 1481 |
-
'رب اشرح لي صدري', 'ربنا آتنا',
|
| 1482 |
-
]
|
| 1483 |
-
_is_religious_text = any(phrase in ctx.current_text for phrase in _RELIGIOUS_PHRASES)
|
| 1484 |
-
if _is_religious_text:
|
| 1485 |
-
logger.info(f"[ANALYZE] Religious text detected — skipping grammar+punctuation")
|
| 1486 |
|
| 1487 |
# ── FIX-03: Structured content protection ──
|
| 1488 |
# Protect URLs, emails, dates, code etc. from grammar model destruction
|
|
@@ -1606,18 +1648,43 @@ def analyze_text():
|
|
| 1606 |
# ── Phase 4 (BUG-033/E10): Grammar output sanity check ──
|
| 1607 |
# Reject grammar corrections that produce a non-word when
|
| 1608 |
# the original was already a valid word. Mirrors spelling filter.
|
| 1609 |
-
|
| 1610 |
-
|
| 1611 |
-
|
| 1612 |
-
|
| 1613 |
-
|
| 1614 |
-
|
| 1615 |
-
|
| 1616 |
-
|
| 1617 |
-
|
| 1618 |
-
|
| 1619 |
-
|
| 1620 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1621 |
|
| 1622 |
# FIX-22: Protect tanween (preserve ً ٌ ٍ from original)
|
| 1623 |
_TANWEEN_CHARS = set('ًٌٍ')
|
|
|
|
| 819 |
# Verb/particle confusion: كان (was) ↔ كأن (as if) = ALWAYS wrong
|
| 820 |
'كان': {'كأن'},
|
| 821 |
'كأن': {'كان'},
|
| 822 |
+
'كانت': {'كأنت'}, # H016: كانت → كأنت = ALWAYS wrong
|
| 823 |
+
'كانوا': {'كأنوا'}, # also block plural form
|
| 824 |
# Preposition confusion: different meanings, both valid
|
| 825 |
'إلى': {'على', 'علي'},
|
| 826 |
'على': {'إلى', 'علي'},
|
|
|
|
| 1287 |
if not run_spelling:
|
| 1288 |
logger.info(f"[ANALYZE] Text length {text_len} > 300 — skipping AraSpell for performance")
|
| 1289 |
|
| 1290 |
+
# ── Batch 2+5: Religious text detection (moved before spelling) ──
|
| 1291 |
+
# Religious text must skip ALL stages (spelling + grammar + punctuation)
|
| 1292 |
+
# to prevent ه→ة corruption (إله→إلة, لسانه→لسانة, etc.)
|
| 1293 |
+
_RELIGIOUS_PHRASES = [
|
| 1294 |
+
# Quran opening/common
|
| 1295 |
+
'بسم الله', 'الحمد لله', 'سبحان الله', 'لا إله إلا الله',
|
| 1296 |
+
'إياك نعبد', 'قل هو الله', 'قل أعوذ', 'إنا أنزلناه',
|
| 1297 |
+
'حسبنا الله', 'لا حول ولا قوة', 'أستغفر الله',
|
| 1298 |
+
'الله أكبر', 'إنا لله', 'اللهم صل', 'وإياك نستعين',
|
| 1299 |
+
'ذلك الكتاب لا ريب', 'مالك يوم الدين', 'لم يلد ولم يولد',
|
| 1300 |
+
'الله لا إله إلا هو', 'الرحمن الرحيم', 'رب العالمين',
|
| 1301 |
+
'إنما الأعمال بالنيات', 'السلام عليكم ورحمة الله',
|
| 1302 |
+
'صراط الذين أنعمت', 'من شر ما خلق', 'ملك الناس',
|
| 1303 |
+
'رب اشرح لي صدري', 'ربنا آتنا',
|
| 1304 |
+
'قل أعوذ برب الناس', 'الحي القيوم',
|
| 1305 |
+
'لا تأخذه سنة ولا نوم', 'أشهد أن لا إله',
|
| 1306 |
+
'أشهد أن محمد', 'إنما الأعمال',
|
| 1307 |
+
'من حسن إسلام المرء', 'سبحان الله وبحمده',
|
| 1308 |
+
'الله أكبر كبير', 'إله الناس', 'من شر الوسواس',
|
| 1309 |
+
'وأشهد أن', 'رسول الله', 'كرسيه السماوات',
|
| 1310 |
+
'وسع كرسيه', 'في السماوات وما في الأرض',
|
| 1311 |
+
'عليه وسلم', 'صلى الله عليه',
|
| 1312 |
+
'المسلم من سلم المسلمون', # R016
|
| 1313 |
+
'لا يؤمن أحدكم', # R017
|
| 1314 |
+
'اهدنا الصراط', # R004 Fatiha
|
| 1315 |
+
]
|
| 1316 |
+
_is_religious_text = any(phrase in ctx.current_text for phrase in _RELIGIOUS_PHRASES)
|
| 1317 |
+
if _is_religious_text:
|
| 1318 |
+
logger.info(f"[ANALYZE] Religious text detected — skipping ALL stages")
|
| 1319 |
+
# Skip ALL stages for religious text
|
| 1320 |
+
run_spelling = False
|
| 1321 |
+
|
| 1322 |
+
# ── Batch 5: Skip spelling for text containing URLs/emails ──
|
| 1323 |
+
# The spelling model destroys URLs (https→htps, .com→. com)
|
| 1324 |
+
import re as _re_spell_guard
|
| 1325 |
+
_has_url = bool(_re_spell_guard.search(r'https?://\S+', ctx.current_text))
|
| 1326 |
+
_has_email = bool(_re_spell_guard.search(r'\S+@\S+\.\S+', ctx.current_text))
|
| 1327 |
+
_has_hashtag = bool(_re_spell_guard.search(r'#[\u0600-\u06FF\w]{2,}', ctx.current_text))
|
| 1328 |
+
_has_percent = bool(_re_spell_guard.search(r'\d+\.\d+%', ctx.current_text))
|
| 1329 |
+
_has_latin_word = bool(_re_spell_guard.search(r'\b[A-Za-z]{3,}\b', ctx.current_text))
|
| 1330 |
+
if _has_url or _has_email:
|
| 1331 |
+
logger.info(f"[ANALYZE] Text contains URLs/emails — skipping spelling")
|
| 1332 |
+
run_spelling = False
|
| 1333 |
+
elif _has_latin_word:
|
| 1334 |
+
logger.info(f"[ANALYZE] Text contains Latin words — skipping spelling")
|
| 1335 |
+
run_spelling = False
|
| 1336 |
+
elif _has_hashtag:
|
| 1337 |
+
logger.info(f"[ANALYZE] Text contains hashtags — skipping spelling")
|
| 1338 |
+
run_spelling = False
|
| 1339 |
+
elif _has_percent:
|
| 1340 |
+
logger.info(f"[ANALYZE] Text contains percentages — skipping spelling")
|
| 1341 |
+
run_spelling = False
|
| 1342 |
+
|
| 1343 |
# 1. Spelling (with conservative post-filtering to avoid over-editing)
|
| 1344 |
if run_spelling:
|
| 1345 |
try:
|
|
|
|
| 1523 |
logger.error(traceback.format_exc())
|
| 1524 |
timing_ms['spelling_error'] = f"{type(e).__name__}: {str(e)[:200]}"
|
| 1525 |
|
| 1526 |
+
# ── FIX-07: Religious text already detected above (before spelling) ──
|
| 1527 |
+
# _is_religious_text was set earlier to skip ALL stages for sacred text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1528 |
|
| 1529 |
# ── FIX-03: Structured content protection ──
|
| 1530 |
# Protect URLs, emails, dates, code etc. from grammar model destruction
|
|
|
|
| 1648 |
# ── Phase 4 (BUG-033/E10): Grammar output sanity check ──
|
| 1649 |
# Reject grammar corrections that produce a non-word when
|
| 1650 |
# the original was already a valid word. Mirrors spelling filter.
|
| 1651 |
+
# BUT: bypass for known grammar patterns (case endings, nasb, etc.)
|
| 1652 |
+
_is_grammar_pattern = False
|
| 1653 |
+
if orig_text and corr_text:
|
| 1654 |
+
# Case: ون/ان → ين (sound masculine plural case)
|
| 1655 |
+
if (orig_text.endswith('ون') and corr_text.endswith('ين') and
|
| 1656 |
+
orig_text[:-2] == corr_text[:-2]):
|
| 1657 |
+
_is_grammar_pattern = True
|
| 1658 |
+
# Nasb/Jazm: ون → وا (verb mood)
|
| 1659 |
+
elif (orig_text.endswith('ون') and corr_text.endswith('وا') and
|
| 1660 |
+
orig_text[:-2] == corr_text[:-2]):
|
| 1661 |
+
_is_grammar_pattern = True
|
| 1662 |
+
# Five nouns: وك → اك/يك (أبوك→أباك, أخوك→أخيك)
|
| 1663 |
+
elif (len(orig_text) >= 3 and len(corr_text) >= 3 and
|
| 1664 |
+
orig_text[-2:] in ('وك', 'وه') and
|
| 1665 |
+
corr_text[-2:] in ('اك', 'يك', 'اه', 'يه')):
|
| 1666 |
+
_is_grammar_pattern = True
|
| 1667 |
+
# Dual: ان → ين (dual oblique)
|
| 1668 |
+
elif (orig_text.endswith('ان') and corr_text.endswith('ين') and
|
| 1669 |
+
orig_text[:-2] == corr_text[:-2] and len(orig_text) >= 4):
|
| 1670 |
+
_is_grammar_pattern = True
|
| 1671 |
+
# Demonstrative: هذان→هاتان, هاتان→هذان
|
| 1672 |
+
elif ({orig_text, corr_text} <= {'هذان', 'هاتان'}):
|
| 1673 |
+
_is_grammar_pattern = True
|
| 1674 |
+
|
| 1675 |
+
if not _is_grammar_pattern:
|
| 1676 |
+
if len(orig_text.split()) == 1 and len(corr_text.split()) == 1:
|
| 1677 |
+
try:
|
| 1678 |
+
from nlp.spelling.araspell_service import get_spelling_model
|
| 1679 |
+
_vm = get_spelling_model().vocab_manager
|
| 1680 |
+
if _vm and _vm.is_iv(orig_text) and _vm.is_oov(corr_text):
|
| 1681 |
+
logger.info(
|
| 1682 |
+
f"[GRAMMAR] Rejected corruption: '{orig_text}'→'{corr_text}' "
|
| 1683 |
+
f"(valid word → non-word)"
|
| 1684 |
+
)
|
| 1685 |
+
continue
|
| 1686 |
+
except Exception:
|
| 1687 |
+
pass
|
| 1688 |
|
| 1689 |
# FIX-22: Protect tanween (preserve ً ٌ ٍ from original)
|
| 1690 |
_TANWEEN_CHARS = set('ًٌٍ')
|
src/nlp/grammar/grammar_rules.py
CHANGED
|
@@ -146,6 +146,40 @@ class ArabicGrammarGuard:
|
|
| 146 |
|
| 147 |
text = re.sub(r'\bإحدى عشرة\s+([أ-ي]+ا|رجل[اأ]|طالب[اأ]|مهندس[اأ])\b', r'أحد عشر \1', text)
|
| 148 |
text = re.sub(r'\bإحدى عشر\s+([أ-ي]+ا|رجل[اأ]|طالب[اأ]|مهندس[اأ])\b', r'أحد عشر \1', text)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
return text
|
| 150 |
|
| 151 |
def fix_prepositions_advanced(self, text):
|
|
@@ -279,14 +313,27 @@ class ArabicGrammarGuard:
|
|
| 279 |
continue
|
| 280 |
|
| 281 |
# Fix the verb to agree with the plural subject
|
| 282 |
-
if
|
| 283 |
-
|
| 284 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 285 |
corrected_tokens[i+1] = verb_word + 'ن'
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
if len(verb_word) >= 3 and not verb_word.startswith('ي') and not verb_word.startswith('ت'):
|
| 290 |
corrected_tokens[i+1] = verb_word + 'وا'
|
| 291 |
|
| 292 |
return " ".join(corrected_tokens)
|
|
|
|
| 146 |
|
| 147 |
text = re.sub(r'\bإحدى عشرة\s+([أ-ي]+ا|رجل[اأ]|طالب[اأ]|مهندس[اأ])\b', r'أحد عشر \1', text)
|
| 148 |
text = re.sub(r'\bإحدى عشر\s+([أ-ي]+ا|رجل[اأ]|طالب[اأ]|مهندس[اأ])\b', r'أحد عشر \1', text)
|
| 149 |
+
|
| 150 |
+
# ── Batch 6: Noun-adjective gender agreement ──
|
| 151 |
+
# When a feminine noun is followed by a masculine adjective, add ة
|
| 152 |
+
# e.g. السيارة جميل → السيارة جميلة
|
| 153 |
+
KNOWN_FEMININE_NOUNS = {
|
| 154 |
+
'السيارة', 'سيارة', 'المدرسة', 'مدرسة', 'المدينة', 'مدينة',
|
| 155 |
+
'البنت', 'الشمس', 'الأرض', 'الطالبة', 'طالبة',
|
| 156 |
+
'الجامعة', 'جامعة', 'الشركة', 'شركة', 'الحكومة', 'حكومة',
|
| 157 |
+
'الغرفة', 'غرفة', 'الحديقة', 'حديقة', 'المكتبة', 'مكتبة',
|
| 158 |
+
'الدولة', 'دولة', 'الرحلة', 'رحلة', 'اللغة', 'لغة',
|
| 159 |
+
'القصة', 'قصة', 'الفكرة', 'فكرة', 'النتيجة', 'نتيجة',
|
| 160 |
+
}
|
| 161 |
+
# Common adjectives that have masculine/feminine pairs
|
| 162 |
+
MASC_TO_FEM_ADJ = {
|
| 163 |
+
'جميل': 'جميلة', 'كبير': 'كبيرة', 'صغير': 'صغيرة',
|
| 164 |
+
'طويل': 'طويلة', 'قصير': 'قصيرة', 'جديد': 'جديدة',
|
| 165 |
+
'قديم': 'قديمة', 'بعيد': 'بعيدة', 'قريب': 'قريبة',
|
| 166 |
+
'سريع': 'سريعة', 'بطيء': 'بطيئة', 'واسع': 'واسعة',
|
| 167 |
+
'ضيق': 'ضيقة', 'عميق': 'عميقة', 'خفيف': 'خفيفة',
|
| 168 |
+
'ثقيل': 'ثقيلة', 'نظيف': 'نظيفة', 'مشرق': 'مشرقة',
|
| 169 |
+
'ذكي': 'ذكية', 'غني': 'غنية', 'فقير': 'فقيرة',
|
| 170 |
+
'متفوق': 'متفوقة', 'مجتهد': 'مجتهدة', 'ممتاز': 'ممتازة',
|
| 171 |
+
}
|
| 172 |
+
words = text.split()
|
| 173 |
+
for i in range(len(words) - 1):
|
| 174 |
+
noun = words[i]
|
| 175 |
+
adj = words[i + 1]
|
| 176 |
+
is_fem_noun = (noun in KNOWN_FEMININE_NOUNS or
|
| 177 |
+
(noun.endswith('ة') and len(noun) >= 3) or
|
| 178 |
+
(noun.startswith('ال') and noun.endswith('ة')))
|
| 179 |
+
if is_fem_noun and adj in MASC_TO_FEM_ADJ:
|
| 180 |
+
words[i + 1] = MASC_TO_FEM_ADJ[adj]
|
| 181 |
+
text = ' '.join(words)
|
| 182 |
+
|
| 183 |
return text
|
| 184 |
|
| 185 |
def fix_prepositions_advanced(self, text):
|
|
|
|
| 313 |
continue
|
| 314 |
|
| 315 |
# Fix the verb to agree with the plural subject
|
| 316 |
+
# Detect if verb is present tense (starts with ي/ت/ن/أ)
|
| 317 |
+
_is_present = (verb_word.startswith('ي') or verb_word.startswith('ت')
|
| 318 |
+
or verb_word.startswith('ن') or verb_word.startswith('أ'))
|
| 319 |
+
|
| 320 |
+
if _is_present:
|
| 321 |
+
# Present tense: يذهب→يذهبون (masc) / يذهبن (fem)
|
| 322 |
+
if is_plural_fem:
|
| 323 |
+
if not verb_word.endswith('ن') and not verb_word.endswith('نَ'):
|
| 324 |
+
corrected_tokens[i+1] = verb_word + 'ن'
|
| 325 |
+
elif is_plural_masc:
|
| 326 |
+
if (not verb_word.endswith('ون') and not verb_word.endswith('وا')
|
| 327 |
+
and not verb_word.endswith('ين')):
|
| 328 |
+
corrected_tokens[i+1] = verb_word + 'ون'
|
| 329 |
+
else:
|
| 330 |
+
# Past tense: ذهب→ذهبوا (masc) / ذهبن (fem)
|
| 331 |
+
if is_plural_fem:
|
| 332 |
+
if not verb_word.endswith('ن') and not verb_word.endswith('نَ'):
|
| 333 |
corrected_tokens[i+1] = verb_word + 'ن'
|
| 334 |
+
elif is_plural_masc:
|
| 335 |
+
if (not verb_word.endswith('وا') and not verb_word.endswith('ون')
|
| 336 |
+
and not verb_word.endswith('ين')):
|
|
|
|
| 337 |
corrected_tokens[i+1] = verb_word + 'وا'
|
| 338 |
|
| 339 |
return " ".join(corrected_tokens)
|
src/nlp/punctuation/punctuation_rules.py
CHANGED
|
@@ -138,6 +138,45 @@ def validate_punctuation_diff(diff: dict) -> bool:
|
|
| 138 |
)
|
| 139 |
return False
|
| 140 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
# ── Rule 1: Alphabetic content must be identical after normalization ──
|
| 142 |
orig_alpha = re.sub(r'[.,،؛؟!:;?\s]', '', original)
|
| 143 |
corr_alpha = re.sub(r'[.,،؛؟!:;?\s]', '', correction)
|
|
|
|
| 138 |
)
|
| 139 |
return False
|
| 140 |
|
| 141 |
+
# ── Rule 0b (Batch 4): Reject punct insertion when original has no punctuation ──
|
| 142 |
+
# If the original text has zero Arabic punctuation and the correction
|
| 143 |
+
# only adds commas/semicolons (not at the very end), it's overcorrection.
|
| 144 |
+
# This catches "already correct" texts that PuncAra sprinkles with commas.
|
| 145 |
+
orig_punct_count_r0b = sum(1 for c in original if c in ARABIC_PUNCT_CHARS)
|
| 146 |
+
if orig_punct_count_r0b == 0:
|
| 147 |
+
corr_punct_count_r0b = sum(1 for c in correction if c in ARABIC_PUNCT_CHARS)
|
| 148 |
+
if corr_punct_count_r0b > 0:
|
| 149 |
+
# Only allow if adding a single period/question at the very end
|
| 150 |
+
stripped_corr = correction.rstrip()
|
| 151 |
+
if stripped_corr and stripped_corr[-1] in '.؟?!':
|
| 152 |
+
# This is terminal punct (already handled by Rule 0)
|
| 153 |
+
pass
|
| 154 |
+
else:
|
| 155 |
+
# Mid-sentence punct insertion on a clean sentence → reject
|
| 156 |
+
logger.info(
|
| 157 |
+
f"[PUNC-SAFETY] Rejected mid-sentence punct insertion on clean text: "
|
| 158 |
+
f"'{original}' → '{correction}'"
|
| 159 |
+
)
|
| 160 |
+
return False
|
| 161 |
+
|
| 162 |
+
# ── Rule 0c (Batch 4): Reject punctuation rearrangement ──
|
| 163 |
+
# When original already has punctuation and the correction merely MOVES
|
| 164 |
+
# marks to different positions (same count, same chars), reject.
|
| 165 |
+
# e.g. "حالك؟ أنا" → "حالك أنا؟" = rearrangement
|
| 166 |
+
orig_punct_count_r0c = sum(1 for c in original if c in ARABIC_PUNCT_CHARS)
|
| 167 |
+
corr_punct_count_r0c = sum(1 for c in correction if c in ARABIC_PUNCT_CHARS)
|
| 168 |
+
if orig_punct_count_r0c > 0 and corr_punct_count_r0c > 0:
|
| 169 |
+
# Both have punctuation — check if it's just rearrangement
|
| 170 |
+
orig_punct_set = sorted(c for c in original if c in ARABIC_PUNCT_CHARS)
|
| 171 |
+
corr_punct_set = sorted(c for c in correction if c in ARABIC_PUNCT_CHARS)
|
| 172 |
+
if orig_punct_set == corr_punct_set:
|
| 173 |
+
# Same punct chars — only positions changed. This is rearrangement.
|
| 174 |
+
logger.info(
|
| 175 |
+
f"[PUNC-SAFETY] Rejected punct rearrangement: "
|
| 176 |
+
f"'{original}' → '{correction}'"
|
| 177 |
+
)
|
| 178 |
+
return False
|
| 179 |
+
|
| 180 |
# ── Rule 1: Alphabetic content must be identical after normalization ──
|
| 181 |
orig_alpha = re.sub(r'[.,،؛؟!:;?\s]', '', original)
|
| 182 |
corr_alpha = re.sub(r'[.,،؛؟!:;?\s]', '', correction)
|
src/nlp/spelling/araspell_rules.py
CHANGED
|
@@ -169,7 +169,6 @@ class AraSpellPostProcessor:
|
|
| 169 |
'احيانا': 'أحياناً', 'احياناً': 'أحياناً',
|
| 170 |
'ابدا': 'أبداً', 'ابداً': 'أبداً',
|
| 171 |
'اصلا': 'أصلاً', 'اصلاً': 'أصلاً',
|
| 172 |
-
'اخيرا': 'أخيراً',
|
| 173 |
'اخبار': 'أخبار', 'اخبر': 'أخبر',
|
| 174 |
'امر': 'أمر', 'امور': 'أمور',
|
| 175 |
'اهم': 'أهم', 'اهمية': 'أهمية',
|
|
@@ -177,6 +176,27 @@ class AraSpellPostProcessor:
|
|
| 177 |
'اثر': 'أثر', 'اثار': 'آثار',
|
| 178 |
'اساء': 'أساء', 'اساس': 'أساس',
|
| 179 |
'استاذ': 'أستاذ', 'اسلام': 'إسلام',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
# FIX-14: Alif maqsura common errors
|
| 181 |
'المستشفي': 'المستشفى',
|
| 182 |
'مصطفي': 'مصطفى', 'موسي': 'موسى', 'عيسي': 'عيسى',
|
|
|
|
| 169 |
'احيانا': 'أحياناً', 'احياناً': 'أحياناً',
|
| 170 |
'ابدا': 'أبداً', 'ابداً': 'أبداً',
|
| 171 |
'اصلا': 'أصلاً', 'اصلاً': 'أصلاً',
|
|
|
|
| 172 |
'اخبار': 'أخبار', 'اخبر': 'أخبر',
|
| 173 |
'امر': 'أمر', 'امور': 'أمور',
|
| 174 |
'اهم': 'أهم', 'اهمية': 'أهمية',
|
|
|
|
| 176 |
'اثر': 'أثر', 'اثار': 'آثار',
|
| 177 |
'اساء': 'أساء', 'اساس': 'أساس',
|
| 178 |
'استاذ': 'أستاذ', 'اسلام': 'إسلام',
|
| 179 |
+
# Batch 3: More hamza entries for remaining FN cases
|
| 180 |
+
'اسرة': 'أسرة', 'اسر': 'أسر',
|
| 181 |
+
'اعضاء': 'أعضاء', 'اعداد': 'أعداد',
|
| 182 |
+
'اعمال': 'أعمال', 'اعمار': 'أعمار',
|
| 183 |
+
'انجاز': 'إنجاز', 'انجازات': 'إنجازات',
|
| 184 |
+
'انشاء': 'إنشاء', 'انتاج': 'إنتاج',
|
| 185 |
+
'انتخابات': 'انتخابات', 'انتظار': 'انتظار',
|
| 186 |
+
'اسلامي': 'إسلامي', 'اسلامية': 'إسلامية',
|
| 187 |
+
'امكانية': 'إمكانية', 'امكان': 'إمكان',
|
| 188 |
+
'اشكالية': 'إشكالية',
|
| 189 |
+
'ادارة': 'إدارة', 'ادارية': 'إدارية',
|
| 190 |
+
'اعلام': 'إعلام', 'اعلامي': 'إعلامي',
|
| 191 |
+
'احتمال': 'احتمال', 'احتفال': 'احتفال',
|
| 192 |
+
'ازور': 'أزور', 'اذهب': 'أذهب', 'اكتب': 'أكتب',
|
| 193 |
+
'اقرا': 'أقرأ', 'اقرأ': 'أقرأ',
|
| 194 |
+
'اعمل': 'أعمل', 'ادرس': 'أدرس',
|
| 195 |
+
'اشتري': 'أشتري', 'اسافر': 'أسافر',
|
| 196 |
+
'مسؤول': 'مسؤول', 'مسؤولية': 'مسؤولية',
|
| 197 |
+
'رؤية': 'رؤية', 'رؤيا': 'رؤيا',
|
| 198 |
+
'مؤسسة': 'مؤسسة', 'مؤتمر': 'مؤتمر',
|
| 199 |
+
'تأثير': 'تأثير', 'تأكيد': 'تأكيد',
|
| 200 |
# FIX-14: Alif maqsura common errors
|
| 201 |
'المستشفي': 'المستشفى',
|
| 202 |
'مصطفي': 'مصطفى', 'موسي': 'موسى', 'عيسي': 'عيسى',
|
tests/phase10/benchmark_runner.py
ADDED
|
@@ -0,0 +1,634 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
BAYAN Phase 10 — Unified Benchmark Runner
|
| 3 |
+
==========================================
|
| 4 |
+
Runs ALL gold datasets through raw models AND full pipeline.
|
| 5 |
+
Performs root cause attribution for every failure.
|
| 6 |
+
Generates regression analysis and stage interaction matrix.
|
| 7 |
+
|
| 8 |
+
Usage:
|
| 9 |
+
python tests/phase10/benchmark_runner.py [--url URL] [--dataset NAMES] [--out DIR]
|
| 10 |
+
"""
|
| 11 |
+
import argparse, json, time, re, os, sys
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
from dataclasses import dataclass, field, asdict
|
| 14 |
+
from typing import List, Dict, Optional, Any
|
| 15 |
+
import requests
|
| 16 |
+
|
| 17 |
+
DEFAULT_URL = "https://bayan10-bayan-api.hf.space"
|
| 18 |
+
GOLD_DIR = Path(__file__).parent / "gold_datasets"
|
| 19 |
+
REPORT_DIR = Path(__file__).parent / "reports"
|
| 20 |
+
|
| 21 |
+
# ═══════════════════════════════════════════════════════════════
|
| 22 |
+
# API Client
|
| 23 |
+
# ═══════════════════════════════════════════════════════════════
|
| 24 |
+
class API:
|
| 25 |
+
def __init__(self, base):
|
| 26 |
+
self.base = base.rstrip('/')
|
| 27 |
+
self.s = requests.Session()
|
| 28 |
+
self.s.headers['Content-Type'] = 'application/json'
|
| 29 |
+
|
| 30 |
+
def _post(self, ep, payload, timeout=180):
|
| 31 |
+
t0 = time.time()
|
| 32 |
+
try:
|
| 33 |
+
r = self.s.post(f"{self.base}{ep}", json=payload, timeout=timeout)
|
| 34 |
+
ms = int((time.time()-t0)*1000)
|
| 35 |
+
d = r.json(); d['_ms'] = ms; d['_status'] = r.status_code
|
| 36 |
+
return d
|
| 37 |
+
except requests.Timeout:
|
| 38 |
+
return {'error':'TIMEOUT','_ms':int((time.time()-t0)*1000),'_status':0}
|
| 39 |
+
except Exception as e:
|
| 40 |
+
return {'error':str(e),'_ms':int((time.time()-t0)*1000),'_status':0}
|
| 41 |
+
|
| 42 |
+
def analyze(self, text): return self._post('/api/analyze', {'text': text})
|
| 43 |
+
def grammar(self, text): return self._post('/api/grammar', {'text': text})
|
| 44 |
+
def punctuation(self, text): return self._post('/api/punctuation', {'text': text})
|
| 45 |
+
|
| 46 |
+
# ═══════════════════════════════════════════════════════════════
|
| 47 |
+
# Result Types
|
| 48 |
+
# ═══════════════════════════════════════════════════════════════
|
| 49 |
+
@dataclass
|
| 50 |
+
class BenchResult:
|
| 51 |
+
id: str
|
| 52 |
+
dataset: str
|
| 53 |
+
category: str
|
| 54 |
+
input: str
|
| 55 |
+
expected: str = ""
|
| 56 |
+
severity: str = ""
|
| 57 |
+
# Pipeline results
|
| 58 |
+
pipeline_output: str = ""
|
| 59 |
+
pipeline_suggestions: list = field(default_factory=list)
|
| 60 |
+
pipeline_timing: dict = field(default_factory=dict)
|
| 61 |
+
pipeline_ms: int = 0
|
| 62 |
+
# Raw model results
|
| 63 |
+
grammar_raw_output: str = ""
|
| 64 |
+
grammar_raw_ms: int = 0
|
| 65 |
+
punctuation_raw_output: str = ""
|
| 66 |
+
punctuation_raw_ms: int = 0
|
| 67 |
+
# Verdicts
|
| 68 |
+
pipeline_verdict: str = "" # TP, FP, TN, FN, ERROR
|
| 69 |
+
pipeline_detail: str = ""
|
| 70 |
+
# Root cause
|
| 71 |
+
root_cause_component: str = "" # MODEL, RULE, PIPELINE, SPAN, UI, UNKNOWN
|
| 72 |
+
root_cause_stage: str = "" # spelling, grammar, punctuation, integration
|
| 73 |
+
root_cause_detail: str = ""
|
| 74 |
+
# Regression
|
| 75 |
+
regression_type: str = "" # fix_lost, reversal, introduced_error, none
|
| 76 |
+
regression_detail: str = ""
|
| 77 |
+
# Span check
|
| 78 |
+
span_valid: bool = True
|
| 79 |
+
span_detail: str = ""
|
| 80 |
+
|
| 81 |
+
def strip_punct_only(text):
|
| 82 |
+
"""Remove ONLY punctuation chars to compare word content."""
|
| 83 |
+
return re.sub(r'[.,،؛؟!:;?!\s\u060C\u061B\u061F]+', ' ', text).strip()
|
| 84 |
+
|
| 85 |
+
def words(text):
|
| 86 |
+
return re.sub(r'[.,،؛؟!:;?!\s]+', ' ', text).strip().split()
|
| 87 |
+
|
| 88 |
+
# ═══════════════════════════════════════════════════════════════
|
| 89 |
+
# Benchmark Modules
|
| 90 |
+
# ═══════════════════════════════════════════════════════════════
|
| 91 |
+
|
| 92 |
+
def run_spelling_benchmark(api: API, samples: list) -> List[BenchResult]:
|
| 93 |
+
results = []
|
| 94 |
+
for i, s in enumerate(samples):
|
| 95 |
+
print(f" [{i+1}/{len(samples)}] {s['id']} {s.get('category','')}... ", end="", flush=True)
|
| 96 |
+
r = BenchResult(s['id'], 'spelling', s.get('category',''), s['input'],
|
| 97 |
+
s.get('expected',''), s.get('severity',''))
|
| 98 |
+
# Pipeline
|
| 99 |
+
resp = api.analyze(s['input'])
|
| 100 |
+
r.pipeline_ms = resp.get('_ms', 0)
|
| 101 |
+
r.pipeline_timing = resp.get('timing_ms', {})
|
| 102 |
+
if 'error' in resp:
|
| 103 |
+
r.pipeline_verdict = "ERROR"; r.pipeline_detail = resp['error']
|
| 104 |
+
print(f"💥 ERROR"); results.append(r); continue
|
| 105 |
+
|
| 106 |
+
r.pipeline_output = resp.get('corrected', '')
|
| 107 |
+
r.pipeline_suggestions = resp.get('suggestions', [])
|
| 108 |
+
original = resp.get('original', s['input'])
|
| 109 |
+
changed = r.pipeline_output != original
|
| 110 |
+
|
| 111 |
+
error_words = s.get('error_words', [])
|
| 112 |
+
has_errors = len(error_words) > 0
|
| 113 |
+
|
| 114 |
+
# Span check
|
| 115 |
+
for sg in r.pipeline_suggestions:
|
| 116 |
+
actual_slice = original[sg['start']:sg['end']]
|
| 117 |
+
if actual_slice != sg.get('original', ''):
|
| 118 |
+
r.span_valid = False
|
| 119 |
+
r.span_detail = f"SPAN[{sg['start']}:{sg['end']}] exp='{sg.get('original','')}' got='{actual_slice}'"
|
| 120 |
+
break
|
| 121 |
+
|
| 122 |
+
if has_errors:
|
| 123 |
+
unfixed = [w for w in error_words if w in r.pipeline_output]
|
| 124 |
+
if unfixed:
|
| 125 |
+
r.pipeline_verdict = "FN"
|
| 126 |
+
r.pipeline_detail = f"Errors NOT fixed: {unfixed}"
|
| 127 |
+
else:
|
| 128 |
+
r.pipeline_verdict = "TP"
|
| 129 |
+
r.pipeline_detail = f"{len(r.pipeline_suggestions)} fixes"
|
| 130 |
+
else:
|
| 131 |
+
if changed:
|
| 132 |
+
# Check what changed
|
| 133 |
+
sugg_types = [sg.get('type','') for sg in r.pipeline_suggestions]
|
| 134 |
+
changes = [f"{sg.get('original','')}→{sg.get('correction','')}" for sg in r.pipeline_suggestions]
|
| 135 |
+
r.pipeline_verdict = "FP"
|
| 136 |
+
r.pipeline_detail = f"Overcorrected: {changes[:3]}"
|
| 137 |
+
# Root cause: if only punctuation suggestions → punctuation model
|
| 138 |
+
if all(t == 'punctuation' for t in sugg_types):
|
| 139 |
+
r.root_cause_component = "MODEL"
|
| 140 |
+
r.root_cause_stage = "punctuation"
|
| 141 |
+
r.root_cause_detail = "Punctuation model added marks to correct text"
|
| 142 |
+
elif any(t == 'grammar' for t in sugg_types):
|
| 143 |
+
r.root_cause_component = "MODEL"
|
| 144 |
+
r.root_cause_stage = "grammar"
|
| 145 |
+
r.root_cause_detail = "Grammar model made unnecessary changes"
|
| 146 |
+
else:
|
| 147 |
+
r.root_cause_component = "MODEL"
|
| 148 |
+
r.root_cause_stage = "spelling"
|
| 149 |
+
r.root_cause_detail = "Spelling model overcorrected"
|
| 150 |
+
else:
|
| 151 |
+
r.pipeline_verdict = "TN"
|
| 152 |
+
r.pipeline_detail = "Correctly unchanged"
|
| 153 |
+
|
| 154 |
+
# Root cause for FN
|
| 155 |
+
if r.pipeline_verdict == "FN":
|
| 156 |
+
r.root_cause_component = "MODEL"
|
| 157 |
+
r.root_cause_stage = "spelling"
|
| 158 |
+
r.root_cause_detail = f"Spelling model missed: {s.get('error_words',[])}"
|
| 159 |
+
|
| 160 |
+
icon = {"TP":"✅","TN":"✅","FP":"❌","FN":"⚠️","ERROR":"💥"}.get(r.pipeline_verdict,"?")
|
| 161 |
+
print(f"{icon} {r.pipeline_verdict} ({r.pipeline_ms}ms)")
|
| 162 |
+
results.append(r)
|
| 163 |
+
return results
|
| 164 |
+
|
| 165 |
+
def run_grammar_benchmark(api: API, samples: list) -> List[BenchResult]:
|
| 166 |
+
results = []
|
| 167 |
+
for i, s in enumerate(samples):
|
| 168 |
+
print(f" [{i+1}/{len(samples)}] {s['id']} {s.get('category','')}... ", end="", flush=True)
|
| 169 |
+
r = BenchResult(s['id'], 'grammar', s.get('category',''), s['input'],
|
| 170 |
+
s.get('expected_fix',''), s.get('severity',''))
|
| 171 |
+
|
| 172 |
+
# Raw grammar
|
| 173 |
+
resp_g = api.grammar(s['input'])
|
| 174 |
+
r.grammar_raw_ms = resp_g.get('_ms', 0)
|
| 175 |
+
r.grammar_raw_output = resp_g.get('corrected_text', resp_g.get('corrected', ''))
|
| 176 |
+
|
| 177 |
+
# Pipeline
|
| 178 |
+
resp = api.analyze(s['input'])
|
| 179 |
+
r.pipeline_ms = resp.get('_ms', 0)
|
| 180 |
+
r.pipeline_timing = resp.get('timing_ms', {})
|
| 181 |
+
if 'error' in resp:
|
| 182 |
+
r.pipeline_verdict = "ERROR"; r.pipeline_detail = resp.get('error','')
|
| 183 |
+
print(f"💥 ERROR"); results.append(r); continue
|
| 184 |
+
|
| 185 |
+
r.pipeline_output = resp.get('corrected', '')
|
| 186 |
+
r.pipeline_suggestions = resp.get('suggestions', [])
|
| 187 |
+
original = resp.get('original', s['input'])
|
| 188 |
+
changed = r.pipeline_output != original
|
| 189 |
+
error_words = s.get('error_words', [])
|
| 190 |
+
has_errors = len(error_words) > 0
|
| 191 |
+
|
| 192 |
+
# Span check
|
| 193 |
+
for sg in r.pipeline_suggestions:
|
| 194 |
+
actual_slice = original[sg['start']:sg['end']]
|
| 195 |
+
if actual_slice != sg.get('original', ''):
|
| 196 |
+
r.span_valid = False
|
| 197 |
+
r.span_detail = f"SPAN mismatch"
|
| 198 |
+
break
|
| 199 |
+
|
| 200 |
+
if has_errors:
|
| 201 |
+
unfixed = [w for w in error_words if w in r.pipeline_output]
|
| 202 |
+
if unfixed:
|
| 203 |
+
r.pipeline_verdict = "FN"
|
| 204 |
+
r.pipeline_detail = f"Errors NOT fixed: {unfixed}"
|
| 205 |
+
# Root cause: did raw grammar fix it?
|
| 206 |
+
raw_fixed = all(w not in r.grammar_raw_output for w in error_words)
|
| 207 |
+
if raw_fixed:
|
| 208 |
+
r.root_cause_component = "PIPELINE"
|
| 209 |
+
r.root_cause_stage = "integration"
|
| 210 |
+
r.root_cause_detail = "Grammar model fixed it but pipeline lost the fix"
|
| 211 |
+
else:
|
| 212 |
+
r.root_cause_component = "MODEL"
|
| 213 |
+
r.root_cause_stage = "grammar"
|
| 214 |
+
r.root_cause_detail = f"Grammar model did not fix: {unfixed}"
|
| 215 |
+
else:
|
| 216 |
+
r.pipeline_verdict = "TP"
|
| 217 |
+
r.pipeline_detail = f"Fixed"
|
| 218 |
+
else:
|
| 219 |
+
if changed:
|
| 220 |
+
sugg_types = [sg.get('type','') for sg in r.pipeline_suggestions]
|
| 221 |
+
changes = [f"{sg.get('original','')}→{sg.get('correction','')}" for sg in r.pipeline_suggestions]
|
| 222 |
+
r.pipeline_verdict = "FP"
|
| 223 |
+
r.pipeline_detail = f"Overcorrected: {changes[:3]}"
|
| 224 |
+
if all(t == 'punctuation' for t in sugg_types):
|
| 225 |
+
r.root_cause_component = "MODEL"
|
| 226 |
+
r.root_cause_stage = "punctuation"
|
| 227 |
+
r.root_cause_detail = "Punctuation over-injection on correct grammar text"
|
| 228 |
+
else:
|
| 229 |
+
raw_changed = r.grammar_raw_output != s['input']
|
| 230 |
+
if raw_changed:
|
| 231 |
+
r.root_cause_component = "MODEL"
|
| 232 |
+
r.root_cause_stage = "grammar"
|
| 233 |
+
r.root_cause_detail = f"Grammar model hallucinated"
|
| 234 |
+
else:
|
| 235 |
+
r.root_cause_component = "MODEL"
|
| 236 |
+
r.root_cause_stage = "punctuation"
|
| 237 |
+
r.root_cause_detail = "Punctuation model caused FP"
|
| 238 |
+
else:
|
| 239 |
+
r.pipeline_verdict = "TN"
|
| 240 |
+
r.pipeline_detail = "Correctly unchanged"
|
| 241 |
+
|
| 242 |
+
# Regression: did grammar fix get lost in pipeline?
|
| 243 |
+
if has_errors and r.grammar_raw_output != s['input']:
|
| 244 |
+
raw_fixed_words = [w for w in error_words if w not in r.grammar_raw_output]
|
| 245 |
+
pipeline_fixed = [w for w in error_words if w not in r.pipeline_output]
|
| 246 |
+
lost = set(raw_fixed_words) - set(pipeline_fixed)
|
| 247 |
+
if lost:
|
| 248 |
+
r.regression_type = "fix_lost"
|
| 249 |
+
r.regression_detail = f"Grammar fixed {raw_fixed_words} but pipeline lost {list(lost)}"
|
| 250 |
+
|
| 251 |
+
icon = {"TP":"✅","TN":"✅","FP":"❌","FN":"⚠️","ERROR":"💥"}.get(r.pipeline_verdict,"?")
|
| 252 |
+
print(f"{icon} {r.pipeline_verdict} ({r.pipeline_ms}ms) raw_g={r.grammar_raw_ms}ms")
|
| 253 |
+
results.append(r)
|
| 254 |
+
return results
|
| 255 |
+
|
| 256 |
+
def run_punctuation_benchmark(api: API, samples: list) -> List[BenchResult]:
|
| 257 |
+
results = []
|
| 258 |
+
for i, s in enumerate(samples):
|
| 259 |
+
print(f" [{i+1}/{len(samples)}] {s['id']} {s.get('category','')}... ", end="", flush=True)
|
| 260 |
+
r = BenchResult(s['id'], 'punctuation', s.get('category',''), s['input'],
|
| 261 |
+
severity=s.get('severity',''))
|
| 262 |
+
|
| 263 |
+
# Raw punctuation
|
| 264 |
+
resp_p = api.punctuation(s['input'])
|
| 265 |
+
r.punctuation_raw_ms = resp_p.get('_ms', 0)
|
| 266 |
+
r.punctuation_raw_output = resp_p.get('corrected_text', resp_p.get('corrected', ''))
|
| 267 |
+
|
| 268 |
+
# Pipeline
|
| 269 |
+
resp = api.analyze(s['input'])
|
| 270 |
+
r.pipeline_ms = resp.get('_ms', 0)
|
| 271 |
+
if 'error' in resp:
|
| 272 |
+
r.pipeline_verdict = "ERROR"; r.pipeline_detail = resp.get('error','')
|
| 273 |
+
print(f"💥 ERROR"); results.append(r); continue
|
| 274 |
+
|
| 275 |
+
r.pipeline_output = resp.get('corrected', '')
|
| 276 |
+
r.pipeline_suggestions = resp.get('suggestions', [])
|
| 277 |
+
|
| 278 |
+
should_add = s.get('should_add_punct', False)
|
| 279 |
+
word_pres = s.get('expected_words_unchanged', False)
|
| 280 |
+
|
| 281 |
+
# Check word preservation
|
| 282 |
+
if word_pres or s.get('category') == 'word_preservation':
|
| 283 |
+
orig_words = strip_punct_only(s['input'])
|
| 284 |
+
raw_words = strip_punct_only(r.punctuation_raw_output)
|
| 285 |
+
if orig_words != raw_words:
|
| 286 |
+
r.pipeline_verdict = "FP"
|
| 287 |
+
r.pipeline_detail = f"WORD CHANGE in punct: '{orig_words[:50]}' → '{raw_words[:50]}'"
|
| 288 |
+
r.root_cause_component = "MODEL"
|
| 289 |
+
r.root_cause_stage = "punctuation"
|
| 290 |
+
r.root_cause_detail = "Punctuation model changed words"
|
| 291 |
+
else:
|
| 292 |
+
r.pipeline_verdict = "TN"
|
| 293 |
+
r.pipeline_detail = "Words preserved"
|
| 294 |
+
elif should_add:
|
| 295 |
+
if r.punctuation_raw_output != s['input']:
|
| 296 |
+
r.pipeline_verdict = "TP"; r.pipeline_detail = "Punctuation added"
|
| 297 |
+
else:
|
| 298 |
+
r.pipeline_verdict = "FN"; r.pipeline_detail = "No punctuation added"
|
| 299 |
+
r.root_cause_component = "MODEL"
|
| 300 |
+
r.root_cause_stage = "punctuation"
|
| 301 |
+
r.root_cause_detail = "Model failed to add punctuation"
|
| 302 |
+
else:
|
| 303 |
+
if r.punctuation_raw_output != s['input']:
|
| 304 |
+
r.pipeline_verdict = "FP"
|
| 305 |
+
r.pipeline_detail = f"Over-punctuated: '{r.punctuation_raw_output[:60]}'"
|
| 306 |
+
r.root_cause_component = "MODEL"
|
| 307 |
+
r.root_cause_stage = "punctuation"
|
| 308 |
+
r.root_cause_detail = "Model modified already-punctuated text"
|
| 309 |
+
else:
|
| 310 |
+
r.pipeline_verdict = "TN"; r.pipeline_detail = "Correctly unchanged"
|
| 311 |
+
|
| 312 |
+
icon = {"TP":"✅","TN":"✅","FP":"❌","FN":"⚠️","ERROR":"💥"}.get(r.pipeline_verdict,"?")
|
| 313 |
+
print(f"{icon} {r.pipeline_verdict} ({r.punctuation_raw_ms}ms)")
|
| 314 |
+
results.append(r)
|
| 315 |
+
return results
|
| 316 |
+
|
| 317 |
+
def run_entity_benchmark(api: API, samples: list) -> List[BenchResult]:
|
| 318 |
+
results = []
|
| 319 |
+
for i, s in enumerate(samples):
|
| 320 |
+
print(f" [{i+1}/{len(samples)}] {s['id']} {s.get('category','')}... ", end="", flush=True)
|
| 321 |
+
r = BenchResult(s['id'], 'entities', s.get('category',''), s['input'],
|
| 322 |
+
severity=s.get('severity',''))
|
| 323 |
+
|
| 324 |
+
resp = api.analyze(s['input'])
|
| 325 |
+
r.pipeline_ms = resp.get('_ms', 0)
|
| 326 |
+
if 'error' in resp:
|
| 327 |
+
r.pipeline_verdict = "ERROR"; r.pipeline_detail = resp.get('error','')
|
| 328 |
+
print(f"💥 ERROR"); results.append(r); continue
|
| 329 |
+
|
| 330 |
+
r.pipeline_output = resp.get('corrected', '')
|
| 331 |
+
r.pipeline_suggestions = resp.get('suggestions', [])
|
| 332 |
+
entity = s.get('entity', '')
|
| 333 |
+
|
| 334 |
+
if entity and entity not in r.pipeline_output:
|
| 335 |
+
r.pipeline_verdict = "FP"
|
| 336 |
+
r.pipeline_detail = f"ENTITY CORRUPTED: '{entity}' missing from output"
|
| 337 |
+
r.root_cause_component = "MODEL"
|
| 338 |
+
# Check which stage corrupted it
|
| 339 |
+
sugg_types = [sg.get('type','') for sg in r.pipeline_suggestions]
|
| 340 |
+
if 'grammar' in sugg_types:
|
| 341 |
+
r.root_cause_stage = "grammar"
|
| 342 |
+
elif 'spelling' in sugg_types:
|
| 343 |
+
r.root_cause_stage = "spelling"
|
| 344 |
+
else:
|
| 345 |
+
r.root_cause_stage = "punctuation"
|
| 346 |
+
r.root_cause_detail = f"Entity '{entity}' was modified"
|
| 347 |
+
elif r.pipeline_output != resp.get('original', s['input']):
|
| 348 |
+
changes = [f"{sg.get('original','')}→{sg.get('correction','')}" for sg in r.pipeline_suggestions]
|
| 349 |
+
if changes:
|
| 350 |
+
r.pipeline_verdict = "FP"
|
| 351 |
+
r.pipeline_detail = f"Text modified: {changes[:3]}"
|
| 352 |
+
if all(sg.get('type') == 'punctuation' for sg in r.pipeline_suggestions):
|
| 353 |
+
r.root_cause_component = "MODEL"
|
| 354 |
+
r.root_cause_stage = "punctuation"
|
| 355 |
+
r.root_cause_detail = "Punctuation added to entity context"
|
| 356 |
+
else:
|
| 357 |
+
r.root_cause_component = "MODEL"
|
| 358 |
+
r.root_cause_stage = "grammar"
|
| 359 |
+
r.root_cause_detail = "Grammar modified entity context"
|
| 360 |
+
else:
|
| 361 |
+
r.pipeline_verdict = "TN"; r.pipeline_detail = "Entity preserved"
|
| 362 |
+
else:
|
| 363 |
+
r.pipeline_verdict = "TN"; r.pipeline_detail = "Entity preserved"
|
| 364 |
+
|
| 365 |
+
icon = {"TP":"✅","TN":"✅","FP":"❌","FN":"⚠️","ERROR":"💥"}.get(r.pipeline_verdict,"?")
|
| 366 |
+
print(f"{icon} {r.pipeline_verdict} ({r.pipeline_ms}ms)")
|
| 367 |
+
results.append(r)
|
| 368 |
+
return results
|
| 369 |
+
|
| 370 |
+
def run_religious_benchmark(api: API, samples: list) -> List[BenchResult]:
|
| 371 |
+
results = []
|
| 372 |
+
for i, s in enumerate(samples):
|
| 373 |
+
print(f" [{i+1}/{len(samples)}] {s['id']} {s.get('category','')}... ", end="", flush=True)
|
| 374 |
+
r = BenchResult(s['id'], 'religious', s.get('category',''), s['input'],
|
| 375 |
+
severity=s.get('severity',''))
|
| 376 |
+
|
| 377 |
+
resp = api.analyze(s['input'])
|
| 378 |
+
r.pipeline_ms = resp.get('_ms', 0)
|
| 379 |
+
if 'error' in resp:
|
| 380 |
+
r.pipeline_verdict = "ERROR"; r.pipeline_detail = resp.get('error','')
|
| 381 |
+
print(f"💥 ERROR"); results.append(r); continue
|
| 382 |
+
|
| 383 |
+
r.pipeline_output = resp.get('corrected', '')
|
| 384 |
+
r.pipeline_suggestions = resp.get('suggestions', [])
|
| 385 |
+
original = resp.get('original', s['input'])
|
| 386 |
+
|
| 387 |
+
if r.pipeline_output != original:
|
| 388 |
+
changes = [f"{sg.get('original','')}→{sg.get('correction','')}" for sg in r.pipeline_suggestions]
|
| 389 |
+
r.pipeline_verdict = "FP"
|
| 390 |
+
r.pipeline_detail = f"RELIGIOUS TEXT MODIFIED: {changes[:3]}"
|
| 391 |
+
sugg_types = [sg.get('type','') for sg in r.pipeline_suggestions]
|
| 392 |
+
if all(t == 'punctuation' for t in sugg_types):
|
| 393 |
+
r.root_cause_component = "MODEL"
|
| 394 |
+
r.root_cause_stage = "punctuation"
|
| 395 |
+
r.root_cause_detail = "Punctuation model modified religious text"
|
| 396 |
+
elif any(t == 'grammar' for t in sugg_types):
|
| 397 |
+
r.root_cause_component = "MODEL"
|
| 398 |
+
r.root_cause_stage = "grammar"
|
| 399 |
+
r.root_cause_detail = "Grammar model rewrote religious text"
|
| 400 |
+
else:
|
| 401 |
+
r.root_cause_component = "MODEL"
|
| 402 |
+
r.root_cause_stage = "spelling"
|
| 403 |
+
r.root_cause_detail = "Spelling model modified religious text"
|
| 404 |
+
else:
|
| 405 |
+
r.pipeline_verdict = "TN"; r.pipeline_detail = "Religious text preserved"
|
| 406 |
+
|
| 407 |
+
icon = {"TP":"✅","TN":"✅","FP":"❌","FN":"⚠️","ERROR":"💥"}.get(r.pipeline_verdict,"?")
|
| 408 |
+
print(f"{icon} {r.pipeline_verdict} ({r.pipeline_ms}ms)")
|
| 409 |
+
results.append(r)
|
| 410 |
+
return results
|
| 411 |
+
|
| 412 |
+
def run_structured_benchmark(api: API, samples: list) -> List[BenchResult]:
|
| 413 |
+
results = []
|
| 414 |
+
for i, s in enumerate(samples):
|
| 415 |
+
print(f" [{i+1}/{len(samples)}] {s['id']} {s.get('category','')}... ", end="", flush=True)
|
| 416 |
+
r = BenchResult(s['id'], 'structured', s.get('category',''), s['input'],
|
| 417 |
+
severity=s.get('severity',''))
|
| 418 |
+
|
| 419 |
+
resp = api.analyze(s['input'])
|
| 420 |
+
r.pipeline_ms = resp.get('_ms', 0)
|
| 421 |
+
if 'error' in resp:
|
| 422 |
+
r.pipeline_verdict = "ERROR"; r.pipeline_detail = resp.get('error','')
|
| 423 |
+
print(f"💥 ERROR"); results.append(r); continue
|
| 424 |
+
|
| 425 |
+
r.pipeline_output = resp.get('corrected', '')
|
| 426 |
+
r.pipeline_suggestions = resp.get('suggestions', [])
|
| 427 |
+
protected = s.get('protected', '')
|
| 428 |
+
|
| 429 |
+
if protected and protected not in r.pipeline_output:
|
| 430 |
+
r.pipeline_verdict = "FP"
|
| 431 |
+
r.pipeline_detail = f"STRUCTURED CORRUPTED: '{protected}' destroyed"
|
| 432 |
+
r.root_cause_component = "MODEL"
|
| 433 |
+
r.root_cause_stage = "grammar"
|
| 434 |
+
r.root_cause_detail = f"Grammar model destroyed: {s.get('category','')}"
|
| 435 |
+
elif r.pipeline_output != resp.get('original', s['input']):
|
| 436 |
+
changes = [f"{sg.get('original','')}→{sg.get('correction','')}" for sg in r.pipeline_suggestions]
|
| 437 |
+
r.pipeline_verdict = "FP"
|
| 438 |
+
r.pipeline_detail = f"Modified: {changes[:3]}"
|
| 439 |
+
if all(sg.get('type') == 'punctuation' for sg in r.pipeline_suggestions):
|
| 440 |
+
r.root_cause_component = "MODEL"
|
| 441 |
+
r.root_cause_stage = "punctuation"
|
| 442 |
+
else:
|
| 443 |
+
r.root_cause_component = "MODEL"
|
| 444 |
+
r.root_cause_stage = "grammar"
|
| 445 |
+
r.root_cause_detail = f"Model corrupted structured content: {s.get('category','')}"
|
| 446 |
+
else:
|
| 447 |
+
r.pipeline_verdict = "TN"; r.pipeline_detail = "Structured content preserved"
|
| 448 |
+
|
| 449 |
+
icon = {"TP":"✅","TN":"✅","FP":"❌","FN":"⚠️","ERROR":"💥"}.get(r.pipeline_verdict,"?")
|
| 450 |
+
print(f"{icon} {r.pipeline_verdict} ({r.pipeline_ms}ms)")
|
| 451 |
+
results.append(r)
|
| 452 |
+
return results
|
| 453 |
+
|
| 454 |
+
def run_hallucination_benchmark(api: API, samples: list) -> List[BenchResult]:
|
| 455 |
+
results = []
|
| 456 |
+
for i, s in enumerate(samples):
|
| 457 |
+
print(f" [{i+1}/{len(samples)}] {s['id']} {s.get('category','')}... ", end="", flush=True)
|
| 458 |
+
r = BenchResult(s['id'], 'hallucination', s.get('category',''), s['input'],
|
| 459 |
+
severity=s.get('severity',''))
|
| 460 |
+
|
| 461 |
+
resp = api.analyze(s['input'])
|
| 462 |
+
r.pipeline_ms = resp.get('_ms', 0)
|
| 463 |
+
if 'error' in resp:
|
| 464 |
+
r.pipeline_verdict = "ERROR"; r.pipeline_detail = resp.get('error','')
|
| 465 |
+
print(f"💥 ERROR"); results.append(r); continue
|
| 466 |
+
|
| 467 |
+
r.pipeline_output = resp.get('corrected', '')
|
| 468 |
+
r.pipeline_suggestions = resp.get('suggestions', [])
|
| 469 |
+
original = resp.get('original', s['input'])
|
| 470 |
+
|
| 471 |
+
if r.pipeline_output != original:
|
| 472 |
+
changes = [f"{sg.get('original','')}→{sg.get('correction','')}" for sg in r.pipeline_suggestions]
|
| 473 |
+
r.pipeline_verdict = "FP"
|
| 474 |
+
r.pipeline_detail = f"HALLUCINATION: {changes[:3]}"
|
| 475 |
+
word_orig = strip_punct_only(original)
|
| 476 |
+
word_corr = strip_punct_only(r.pipeline_output)
|
| 477 |
+
if word_orig == word_corr:
|
| 478 |
+
r.root_cause_component = "MODEL"
|
| 479 |
+
r.root_cause_stage = "punctuation"
|
| 480 |
+
r.root_cause_detail = "Punctuation-only hallucination"
|
| 481 |
+
else:
|
| 482 |
+
sugg_types = [sg.get('type','') for sg in r.pipeline_suggestions]
|
| 483 |
+
if any(t == 'grammar' for t in sugg_types):
|
| 484 |
+
r.root_cause_component = "MODEL"
|
| 485 |
+
r.root_cause_stage = "grammar"
|
| 486 |
+
r.root_cause_detail = "Grammar model hallucinated on correct text"
|
| 487 |
+
elif any(t == 'spelling' for t in sugg_types):
|
| 488 |
+
r.root_cause_component = "MODEL"
|
| 489 |
+
r.root_cause_stage = "spelling"
|
| 490 |
+
r.root_cause_detail = "Spelling model hallucinated"
|
| 491 |
+
else:
|
| 492 |
+
r.root_cause_component = "MODEL"
|
| 493 |
+
r.root_cause_stage = "punctuation"
|
| 494 |
+
r.root_cause_detail = "Punctuation model hallucinated"
|
| 495 |
+
else:
|
| 496 |
+
r.pipeline_verdict = "TN"; r.pipeline_detail = "No hallucination"
|
| 497 |
+
|
| 498 |
+
icon = {"TP":"✅","TN":"✅","FP":"❌","FN":"⚠️","ERROR":"💥"}.get(r.pipeline_verdict,"?")
|
| 499 |
+
print(f"{icon} {r.pipeline_verdict} ({r.pipeline_ms}ms)")
|
| 500 |
+
results.append(r)
|
| 501 |
+
return results
|
| 502 |
+
|
| 503 |
+
# ═══════════════════════════════════════════════════════════════
|
| 504 |
+
# Metrics
|
| 505 |
+
# ═══════════════════════════════════════════════════════════════
|
| 506 |
+
def calc_metrics(results: List[BenchResult]) -> dict:
|
| 507 |
+
tp = sum(1 for r in results if r.pipeline_verdict == "TP")
|
| 508 |
+
fp = sum(1 for r in results if r.pipeline_verdict == "FP")
|
| 509 |
+
tn = sum(1 for r in results if r.pipeline_verdict == "TN")
|
| 510 |
+
fn = sum(1 for r in results if r.pipeline_verdict == "FN")
|
| 511 |
+
err = sum(1 for r in results if r.pipeline_verdict == "ERROR")
|
| 512 |
+
total = len(results)
|
| 513 |
+
prec = tp / (tp + fp) if (tp + fp) > 0 else 0
|
| 514 |
+
rec = tp / (tp + fn) if (tp + fn) > 0 else 0
|
| 515 |
+
f1 = 2*prec*rec/(prec+rec) if (prec+rec) > 0 else 0
|
| 516 |
+
lats = sorted([r.pipeline_ms for r in results if r.pipeline_ms > 0])
|
| 517 |
+
return {
|
| 518 |
+
"total": total, "TP": tp, "FP": fp, "TN": tn, "FN": fn, "ERROR": err,
|
| 519 |
+
"precision": round(prec, 4), "recall": round(rec, 4), "f1": round(f1, 4),
|
| 520 |
+
"fpr": round(fp/(fp+tn) if (fp+tn)>0 else 0, 4),
|
| 521 |
+
"fnr": round(fn/(fn+tp) if (fn+tp)>0 else 0, 4),
|
| 522 |
+
"pass_rate": round((tp+tn)/max(1,total), 4),
|
| 523 |
+
"overcorrection_rate": round(fp/max(1,total), 4),
|
| 524 |
+
"undercorrection_rate": round(fn/max(1,total), 4),
|
| 525 |
+
"latency_p50": lats[len(lats)//2] if lats else 0,
|
| 526 |
+
"latency_p95": lats[int(len(lats)*0.95)] if lats else 0,
|
| 527 |
+
}
|
| 528 |
+
|
| 529 |
+
def root_cause_summary(results: List[BenchResult]) -> dict:
|
| 530 |
+
failures = [r for r in results if r.pipeline_verdict in ("FP","FN")]
|
| 531 |
+
by_component = {}
|
| 532 |
+
by_stage = {}
|
| 533 |
+
for r in failures:
|
| 534 |
+
comp = r.root_cause_component or "UNKNOWN"
|
| 535 |
+
stage = r.root_cause_stage or "unknown"
|
| 536 |
+
by_component[comp] = by_component.get(comp, 0) + 1
|
| 537 |
+
key = f"{comp}:{stage}"
|
| 538 |
+
by_stage[key] = by_stage.get(key, 0) + 1
|
| 539 |
+
return {
|
| 540 |
+
"total_failures": len(failures),
|
| 541 |
+
"by_component": dict(sorted(by_component.items(), key=lambda x: -x[1])),
|
| 542 |
+
"by_stage": dict(sorted(by_stage.items(), key=lambda x: -x[1])),
|
| 543 |
+
}
|
| 544 |
+
|
| 545 |
+
def stage_interaction_matrix(results: List[BenchResult]) -> dict:
|
| 546 |
+
conflicts = {"spelling→grammar": 0, "grammar→punctuation": 0, "spelling→punctuation": 0}
|
| 547 |
+
reversions = 0
|
| 548 |
+
for r in results:
|
| 549 |
+
if r.regression_type == "fix_lost":
|
| 550 |
+
reversions += 1
|
| 551 |
+
if "grammar" in r.regression_detail.lower():
|
| 552 |
+
conflicts["spelling→grammar"] += 1
|
| 553 |
+
return {"conflicts": conflicts, "reversions": reversions}
|
| 554 |
+
|
| 555 |
+
# ═══════════════════════════════════════════════════════════════
|
| 556 |
+
# Main Runner
|
| 557 |
+
# ═══════════════════════════════════════════════════════════════
|
| 558 |
+
def main():
|
| 559 |
+
parser = argparse.ArgumentParser()
|
| 560 |
+
parser.add_argument("--url", default=DEFAULT_URL)
|
| 561 |
+
parser.add_argument("--dataset", nargs="*", default=["ALL"])
|
| 562 |
+
parser.add_argument("--out", default=str(REPORT_DIR))
|
| 563 |
+
args = parser.parse_args()
|
| 564 |
+
|
| 565 |
+
api = API(args.url)
|
| 566 |
+
run_all = "ALL" in [d.upper() for d in args.dataset]
|
| 567 |
+
os.makedirs(args.out, exist_ok=True)
|
| 568 |
+
|
| 569 |
+
print(f"[P10] Target: {args.url}")
|
| 570 |
+
print(f"[P10] Datasets: {args.dataset}")
|
| 571 |
+
all_results = []
|
| 572 |
+
all_metrics = {}
|
| 573 |
+
|
| 574 |
+
DATASETS = {
|
| 575 |
+
"spelling": (GOLD_DIR/"spelling.json", run_spelling_benchmark),
|
| 576 |
+
"grammar": (GOLD_DIR/"grammar.json", run_grammar_benchmark),
|
| 577 |
+
"punctuation": (GOLD_DIR/"punctuation.json", run_punctuation_benchmark),
|
| 578 |
+
"entities": (GOLD_DIR/"entities.json", run_entity_benchmark),
|
| 579 |
+
"religious": (GOLD_DIR/"religious.json", run_religious_benchmark),
|
| 580 |
+
"structured": (GOLD_DIR/"structured_content.json", run_structured_benchmark),
|
| 581 |
+
"hallucination":(GOLD_DIR/"hallucination.json", run_hallucination_benchmark),
|
| 582 |
+
}
|
| 583 |
+
|
| 584 |
+
for name, (path, runner) in DATASETS.items():
|
| 585 |
+
if not run_all and name.upper() not in [d.upper() for d in args.dataset]:
|
| 586 |
+
continue
|
| 587 |
+
if not path.exists():
|
| 588 |
+
print(f"\n⚠️ {name}: {path} not found — skipping")
|
| 589 |
+
continue
|
| 590 |
+
with open(path, 'r', encoding='utf-8') as f:
|
| 591 |
+
samples = json.load(f)
|
| 592 |
+
print(f"\n{'='*60}")
|
| 593 |
+
print(f"DATASET: {name.upper()} ({len(samples)} samples)")
|
| 594 |
+
print(f"{'='*60}")
|
| 595 |
+
results = runner(api, samples)
|
| 596 |
+
m = calc_metrics(results)
|
| 597 |
+
all_metrics[name] = m
|
| 598 |
+
all_results.extend(results)
|
| 599 |
+
print(f"\n Pass={m['pass_rate']:.1%} Prec={m['precision']:.3f} Rec={m['recall']:.3f} F1={m['f1']:.3f}")
|
| 600 |
+
print(f" FPR={m['fpr']:.3f} FNR={m['fnr']:.3f} p50={m['latency_p50']}ms p95={m['latency_p95']}ms")
|
| 601 |
+
|
| 602 |
+
# ── Aggregate ──
|
| 603 |
+
print(f"\n{'='*60}")
|
| 604 |
+
print("AGGREGATE RESULTS")
|
| 605 |
+
print(f"{'='*60}")
|
| 606 |
+
agg = calc_metrics(all_results)
|
| 607 |
+
rc = root_cause_summary(all_results)
|
| 608 |
+
sim = stage_interaction_matrix(all_results)
|
| 609 |
+
|
| 610 |
+
print(f" Total: {agg['total']} | Pass: {agg['pass_rate']:.1%}")
|
| 611 |
+
print(f" TP={agg['TP']} TN={agg['TN']} FP={agg['FP']} FN={agg['FN']} ERR={agg['ERROR']}")
|
| 612 |
+
print(f"\n Root Cause by Component: {rc['by_component']}")
|
| 613 |
+
print(f" Root Cause by Stage: {rc['by_stage']}")
|
| 614 |
+
print(f" Stage Conflicts: {sim}")
|
| 615 |
+
|
| 616 |
+
# ── Save ──
|
| 617 |
+
output = {
|
| 618 |
+
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
| 619 |
+
"target": args.url,
|
| 620 |
+
"aggregate_metrics": agg,
|
| 621 |
+
"per_dataset_metrics": all_metrics,
|
| 622 |
+
"root_cause_summary": rc,
|
| 623 |
+
"stage_interactions": sim,
|
| 624 |
+
"total_span_errors": sum(1 for r in all_results if not r.span_valid),
|
| 625 |
+
"total_regressions": sum(1 for r in all_results if r.regression_type),
|
| 626 |
+
"results": [asdict(r) for r in all_results],
|
| 627 |
+
}
|
| 628 |
+
out_path = os.path.join(args.out, "phase10_results.json")
|
| 629 |
+
with open(out_path, 'w', encoding='utf-8') as f:
|
| 630 |
+
json.dump(output, f, ensure_ascii=False, indent=2)
|
| 631 |
+
print(f"\n[P10] Results → {out_path}")
|
| 632 |
+
|
| 633 |
+
if __name__ == "__main__":
|
| 634 |
+
main()
|
tests/phase10/gold_datasets/entities.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{"id":"E001","category":"person","input":"محمد صلاح لاعب كرة قدم مصري","entity":"محمد صلاح","severity":"major"},
|
| 3 |
+
{"id":"E002","category":"person","input":"عبدالله يدرس في الجامعة","entity":"عبدالله","severity":"major"},
|
| 4 |
+
{"id":"E003","category":"person","input":"عبد الرحمن أخي الأكبر","entity":"عبد الرحمن","severity":"major"},
|
| 5 |
+
{"id":"E004","category":"person","input":"أحمد محمود يعمل مهندساً","entity":"أحمد محمود","severity":"major"},
|
| 6 |
+
{"id":"E005","category":"person","input":"الدكتور حسن علي أستاذ جامعي","entity":"حسن علي","severity":"major"},
|
| 7 |
+
{"id":"E006","category":"person","input":"السيدة فاطمة الزهراء معلمة","entity":"فاطمة الزهراء","severity":"major"},
|
| 8 |
+
{"id":"E007","category":"person","input":"الأستاذ عمر بن الخطاب عادل","entity":"عمر بن الخطاب","severity":"major"},
|
| 9 |
+
{"id":"E008","category":"person","input":"خالد بن الوليد قائد عظيم","entity":"خالد بن الوليد","severity":"major"},
|
| 10 |
+
{"id":"E009","category":"person","input":"صلاح الدين الأيوبي حرر القدس","entity":"صلاح الدين الأيوبي","severity":"major"},
|
| 11 |
+
{"id":"E010","category":"person","input":"ابن سينا عالم عربي مشهور","entity":"ابن سينا","severity":"major"},
|
| 12 |
+
{"id":"E011","category":"place","input":"جامعة القاهرة من أعرق الجامعات","entity":"القاهرة","severity":"major"},
|
| 13 |
+
{"id":"E012","category":"place","input":"مدينة الرياض عاصمة المملكة","entity":"الرياض","severity":"major"},
|
| 14 |
+
{"id":"E013","category":"place","input":"دبي مدينة عصرية وجميلة","entity":"دبي","severity":"major"},
|
| 15 |
+
{"id":"E014","category":"place","input":"القدس مدينة مقدسة عند المسلمين","entity":"القدس","severity":"major"},
|
| 16 |
+
{"id":"E015","category":"place","input":"مكة المكرمة أطهر البقاع","entity":"مكة المكرمة","severity":"major"},
|
| 17 |
+
{"id":"E016","category":"place","input":"المدينة المنورة طابة الطيبة","entity":"المدينة المنورة","severity":"major"},
|
| 18 |
+
{"id":"E017","category":"place","input":"بغداد عاصمة العراق","entity":"بغداد","severity":"major"},
|
| 19 |
+
{"id":"E018","category":"place","input":"دمشق أقدم عاصمة في التاريخ","entity":"دمشق","severity":"major"},
|
| 20 |
+
{"id":"E019","category":"company","input":"شركة OpenAI تطور الذكاء الاصطناعي","entity":"OpenAI","severity":"major"},
|
| 21 |
+
{"id":"E020","category":"company","input":"شركة Google عملاق التقنية","entity":"Google","severity":"major"},
|
| 22 |
+
{"id":"E021","category":"company","input":"شركة Microsoft تنتج البرمجيات","entity":"Microsoft","severity":"major"},
|
| 23 |
+
{"id":"E022","category":"company","input":"منصة GitHub للمطورين","entity":"GitHub","severity":"major"},
|
| 24 |
+
{"id":"E023","category":"company","input":"شركة Tesla للسيارات الكهربائية","entity":"Tesla","severity":"major"},
|
| 25 |
+
{"id":"E024","category":"tech","input":"أستخدم Python في البرمجة","entity":"Python","severity":"major"},
|
| 26 |
+
{"id":"E025","category":"tech","input":"إطار TensorFlow مفيد للتعلم","entity":"TensorFlow","severity":"major"},
|
| 27 |
+
{"id":"E026","category":"tech","input":"مكتبة PyTorch للتعلم العميق","entity":"PyTorch","severity":"major"},
|
| 28 |
+
{"id":"E027","category":"tech","input":"منصة Node.js للخوادم","entity":"Node.js","severity":"major"},
|
| 29 |
+
{"id":"E028","category":"tech","input":"لغة JavaScript للويب","entity":"JavaScript","severity":"major"},
|
| 30 |
+
{"id":"E029","category":"tech","input":"قاعدة بيانات MongoDB جيدة","entity":"MongoDB","severity":"major"},
|
| 31 |
+
{"id":"E030","category":"tech","input":"خدمة Docker للحاويات","entity":"Docker","severity":"major"}
|
| 32 |
+
]
|
tests/phase10/gold_datasets/grammar.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{"id":"G001","category":"sv_agree","input":"البنات ذهب إلى المدرسة","expected_fix":"ذهبن/ذهبت","error_words":["ذهب"],"severity":"major"},
|
| 3 |
+
{"id":"G002","category":"sv_agree","input":"الطلاب يذهب إلى الجامعة","expected_fix":"يذهبون","error_words":["يذهب"],"severity":"major"},
|
| 4 |
+
{"id":"G003","category":"sv_agree","input":"المهندسون حضر الاجتماع","expected_fix":"حضروا","error_words":["حضر"],"severity":"major"},
|
| 5 |
+
{"id":"G004","category":"sv_agree","input":"الرجال يعمل في المصنع","expected_fix":"يعملون","error_words":["يعمل"],"severity":"major"},
|
| 6 |
+
{"id":"G005","category":"sv_agree","input":"النساء ذهب إلى السوق","expected_fix":"ذهبن","error_words":["ذهب"],"severity":"major"},
|
| 7 |
+
{"id":"G006","category":"sv_agree","input":"الأولاد لعب في الحديقة","expected_fix":"لعبوا","error_words":["لعب"],"severity":"major"},
|
| 8 |
+
{"id":"G007","category":"sv_agree","input":"المعلمات حضر الاجتماع","expected_fix":"حضرن","error_words":["حضر"],"severity":"major"},
|
| 9 |
+
{"id":"G008","category":"sv_agree","input":"الأطباء يعالج المرضى","expected_fix":"يعالجون","error_words":["يعالج"],"severity":"major"},
|
| 10 |
+
{"id":"G009","category":"sv_agree","input":"العمال بنى المبنى","expected_fix":"بنوا","error_words":["بنى"],"severity":"major"},
|
| 11 |
+
{"id":"G010","category":"sv_agree","input":"الطالبات كتب الواجب","expected_fix":"كتبن","error_words":["كتب"],"severity":"major"},
|
| 12 |
+
{"id":"G011","category":"gender","input":"السيارة جميل جداً","expected_fix":"جميلة","error_words":["جميل"],"severity":"major"},
|
| 13 |
+
{"id":"G012","category":"gender","input":"البنت ذكي في المدرسة","expected_fix":"ذكية","error_words":["ذكي"],"severity":"major"},
|
| 14 |
+
{"id":"G013","category":"gender","input":"الطالبة متفوق في دراسته","expected_fix":"متفوقة/دراستها","error_words":["متفوق","دراسته"],"severity":"major"},
|
| 15 |
+
{"id":"G014","category":"gender","input":"المدينة كبير وواسع","expected_fix":"كبيرة وواسعة","error_words":["كبير","وواسع"],"severity":"major"},
|
| 16 |
+
{"id":"G015","category":"gender","input":"الشمس مشرق اليوم","expected_fix":"مشرقة","error_words":["مشرق"],"severity":"major"},
|
| 17 |
+
{"id":"G016","category":"case","input":"في المهندسون الماهرون","expected_fix":"المهندسين","error_words":["المهندسون"],"severity":"major"},
|
| 18 |
+
{"id":"G017","category":"case","input":"من المعلمون الأكفاء","expected_fix":"المعلمين","error_words":["المعلمون"],"severity":"major"},
|
| 19 |
+
{"id":"G018","category":"case","input":"إلى المسافرون في المطار","expected_fix":"المسافرين","error_words":["المسافرون"],"severity":"major"},
|
| 20 |
+
{"id":"G019","category":"case","input":"على العاملون في المصنع","expected_fix":"العاملين","error_words":["العاملون"],"severity":"major"},
|
| 21 |
+
{"id":"G020","category":"case","input":"عن المهندسون في الشركة","expected_fix":"المهندسين","error_words":["المهندسون"],"severity":"major"},
|
| 22 |
+
{"id":"G021","category":"five_nouns","input":"إن أبوك رجل طيب جداً","expected_fix":"أباك","error_words":["أبوك"],"severity":"major"},
|
| 23 |
+
{"id":"G022","category":"five_nouns","input":"رأيت أخوك في المسجد","expected_fix":"أخاك","error_words":["أخوك"],"severity":"major"},
|
| 24 |
+
{"id":"G023","category":"five_nouns","input":"في أبوك خير كثير","expected_fix":"أبيك","error_words":["أبوك"],"severity":"major"},
|
| 25 |
+
{"id":"G024","category":"five_nouns","input":"على أخوك أن يحضر","expected_fix":"أخيك","error_words":["أخوك"],"severity":"major"},
|
| 26 |
+
{"id":"G025","category":"dual","input":"هذان الطالبتان مجتهدتان","expected_fix":"هاتان","error_words":["هذان"],"severity":"major"},
|
| 27 |
+
{"id":"G026","category":"dual","input":"هاتان الطالبان مجتهدان","expected_fix":"هذان","error_words":["هاتان"],"severity":"major"},
|
| 28 |
+
{"id":"G027","category":"nasb","input":"لن يذهبون إلى المدرسة","expected_fix":"يذهبوا","error_words":["يذهبون"],"severity":"major"},
|
| 29 |
+
{"id":"G028","category":"nasb","input":"لم يفعلون الواجب بعد","expected_fix":"يفعلوا","error_words":["يفعلون"],"severity":"major"},
|
| 30 |
+
{"id":"G029","category":"nasb","input":"كي يتعلمون الدرس","expected_fix":"يتعلموا","error_words":["يتعلمون"],"severity":"major"},
|
| 31 |
+
{"id":"G030","category":"nasb","input":"حتى يعملون بجد","expected_fix":"يعملوا","error_words":["يعملون"],"severity":"major"},
|
| 32 |
+
{"id":"G031","category":"correct","input":"ذهب الطالب إلى المدرسة","expected_fix":"","error_words":[],"severity":"none"},
|
| 33 |
+
{"id":"G032","category":"correct","input":"كتبت الطالبة المقال بنجاح","expected_fix":"","error_words":[],"severity":"none"},
|
| 34 |
+
{"id":"G033","category":"correct","input":"المعلمون في المدرسة يعملون","expected_fix":"","error_words":[],"severity":"none"},
|
| 35 |
+
{"id":"G034","category":"correct","input":"أحب القراءة والكتابة كثيراً","expected_fix":"","error_words":[],"severity":"none"},
|
| 36 |
+
{"id":"G035","category":"correct","input":"ذهبت البنات إلى المدرسة","expected_fix":"","error_words":[],"severity":"none"},
|
| 37 |
+
{"id":"G036","category":"correct","input":"جاء المعلمون إلى الفصل","expected_fix":"","error_words":[],"severity":"none"},
|
| 38 |
+
{"id":"G037","category":"correct","input":"ذهب الرجل إلى عمله","expected_fix":"","error_words":[],"severity":"none"},
|
| 39 |
+
{"id":"G038","category":"correct","input":"يدرس الطالب في مكتبته","expected_fix":"","error_words":[],"severity":"none"},
|
| 40 |
+
{"id":"G039","category":"correct","input":"تعمل المرأة في الشركة","expected_fix":"","error_words":[],"severity":"none"},
|
| 41 |
+
{"id":"G040","category":"correct","input":"نحن نجتهد في العمل","expected_fix":"","error_words":[],"severity":"none"},
|
| 42 |
+
{"id":"G041","category":"correct","input":"هم يسافرون كل صيف","expected_fix":"","error_words":[],"severity":"none"},
|
| 43 |
+
{"id":"G042","category":"correct","input":"الأطفال يلعبون في الحديقة","expected_fix":"","error_words":[],"severity":"none"},
|
| 44 |
+
{"id":"G043","category":"correct","input":"إن العلم نافع للإنسان","expected_fix":"","error_words":[],"severity":"none"},
|
| 45 |
+
{"id":"G044","category":"correct","input":"كان المطر غزيراً أمس","expected_fix":"","error_words":[],"severity":"none"},
|
| 46 |
+
{"id":"G045","category":"correct","input":"لم يحضر الطالب اليوم","expected_fix":"","error_words":[],"severity":"none"}
|
| 47 |
+
]
|
tests/phase10/gold_datasets/hallucination.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{"id":"H001","category":"news","input":"أعلن رئيس الوزراء عن خطة اقتصادية جديدة لتطوير البنية التحتية في البلاد.","must_not_change":true,"severity":"major"},
|
| 3 |
+
{"id":"H002","category":"news","input":"شهدت المنطقة تطورات ميدانية متسارعة خلال الأيام الماضية.","must_not_change":true,"severity":"major"},
|
| 4 |
+
{"id":"H003","category":"news","input":"أكد وزير التعليم أن المناهج الدراسية ستشهد تحديثاً شاملاً.","must_not_change":true,"severity":"major"},
|
| 5 |
+
{"id":"H004","category":"news","input":"افتتح الرئيس مشروعاً جديداً للطاقة المتجددة في الصحراء الغربية.","must_not_change":true,"severity":"major"},
|
| 6 |
+
{"id":"H005","category":"news","input":"حققت الصادرات المصرية نمواً بنسبة عشرة بالمئة خلال الربع الأول.","must_not_change":true,"severity":"major"},
|
| 7 |
+
{"id":"H006","category":"academic","input":"تهدف هذه الدراسة إلى تحليل العوامل المؤثرة في جودة التعليم العالي.","must_not_change":true,"severity":"major"},
|
| 8 |
+
{"id":"H007","category":"academic","input":"أشارت النتائج إلى وجود علاقة إيجابية بين المتغيرين المدروسين.","must_not_change":true,"severity":"major"},
|
| 9 |
+
{"id":"H008","category":"academic","input":"استخدم الباحثون المنهج الوصفي التحليلي لدراسة الظاهرة.","must_not_change":true,"severity":"major"},
|
| 10 |
+
{"id":"H009","category":"academic","input":"تم جمع البيانات من خلال استبانة إلكترونية وزعت على العينة.","must_not_change":true,"severity":"major"},
|
| 11 |
+
{"id":"H010","category":"academic","input":"يوصي الباحث بإجراء دراسات مستقبلية لتعميق الفهم.","must_not_change":true,"severity":"major"},
|
| 12 |
+
{"id":"H011","category":"technical","input":"يعتمد النظام على بنية خادم عميل مع واجهة برمجة تطبيقات.","must_not_change":true,"severity":"major"},
|
| 13 |
+
{"id":"H012","category":"technical","input":"تم تطوير التطبيق باستخدام إطار عمل حديث للواجهة الأمامية.","must_not_change":true,"severity":"major"},
|
| 14 |
+
{"id":"H013","category":"technical","input":"يستخدم النظام خوارزمية التعلم العميق لمعالجة اللغة الطبيعية.","must_not_change":true,"severity":"major"},
|
| 15 |
+
{"id":"H014","category":"legal","input":"وفقاً للمادة الخامسة من القانون المدني يحق للمتضرر المطالبة بالتعويض.","must_not_change":true,"severity":"major"},
|
| 16 |
+
{"id":"H015","category":"legal","input":"يلتزم الطرف الأول بتسليم البضاعة خلال ثلاثين يوماً من تاريخ التعاقد.","must_not_change":true,"severity":"major"},
|
| 17 |
+
{"id":"H016","category":"literary","input":"كانت الشمس تغرب خلف الجبال وتلون السماء بألوان الذهب والأرجوان.","must_not_change":true,"severity":"major"},
|
| 18 |
+
{"id":"H017","category":"literary","input":"في تلك الليلة الهادئة كان صوت الأمواج يعزف لحناً حزيناً.","must_not_change":true,"severity":"major"},
|
| 19 |
+
{"id":"H018","category":"literary","input":"مضى الزمن سريعاً ولم يبق من الذكريات إلا ما حفظته القلوب.","must_not_change":true,"severity":"major"},
|
| 20 |
+
{"id":"H019","category":"correct_simple","input":"الطالب يدرس في المكتبة.","must_not_change":true,"severity":"major"},
|
| 21 |
+
{"id":"H020","category":"correct_simple","input":"ذهبت إلى السوق واشتريت خبزاً.","must_not_change":true,"severity":"major"},
|
| 22 |
+
{"id":"H021","category":"correct_simple","input":"المعلم يشرح الدرس بوضوح.","must_not_change":true,"severity":"major"},
|
| 23 |
+
{"id":"H022","category":"correct_simple","input":"نحن نحب بلادنا ونعمل من أجلها.","must_not_change":true,"severity":"major"},
|
| 24 |
+
{"id":"H023","category":"correct_simple","input":"القراءة تنمي العقل وتوسع المدارك.","must_not_change":true,"severity":"major"},
|
| 25 |
+
{"id":"H024","category":"correct_simple","input":"الماء ضروري للحياة والصحة.","must_not_change":true,"severity":"major"},
|
| 26 |
+
{"id":"H025","category":"correct_simple","input":"يجب أن نحترم الكبير ونعطف على الصغير.","must_not_change":true,"severity":"major"},
|
| 27 |
+
{"id":"H026","category":"correct_compound","input":"إن التعليم هو أساس تقدم الأمم، وبدونه لا يمكن تحقيق التنمية المستدامة.","must_not_change":true,"severity":"major"},
|
| 28 |
+
{"id":"H027","category":"correct_compound","input":"تسعى الحكومة إلى تطوير منظومة التعليم وتحسين جودة المخرجات التعليمية.","must_not_change":true,"severity":"major"},
|
| 29 |
+
{"id":"H028","category":"correct_compound","input":"أثبتت الدراسات العلمية أن ممارسة الرياضة بانتظام تحسن الصحة النفسية والجسدية.","must_not_change":true,"severity":"major"},
|
| 30 |
+
{"id":"H029","category":"correct_compound","input":"يعد الذكاء الاصطناعي من أهم التقنيات الحديثة التي ستغير مستقبل البشرية.","must_not_change":true,"severity":"major"},
|
| 31 |
+
{"id":"H030","category":"correct_compound","input":"تلعب وسائل التواصل الاجتماعي دوراً مهماً في تشكيل الرأي العام المعاصر.","must_not_change":true,"severity":"major"}
|
| 32 |
+
]
|
tests/phase10/gold_datasets/punctuation.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{"id":"P001","category":"missing_period","input":"ذهبت إلى المدرسة ثم عدت إلى البيت","should_add_punct":true,"severity":"minor"},
|
| 3 |
+
{"id":"P002","category":"missing_question","input":"هل أنت بخير يا صديقي","should_add_punct":true,"severity":"minor"},
|
| 4 |
+
{"id":"P003","category":"missing_comma","input":"مرحبا كيف حالك اليوم","should_add_punct":true,"severity":"minor"},
|
| 5 |
+
{"id":"P004","category":"missing_multi","input":"كيف حالك أنا بخير والحمد لله","should_add_punct":true,"severity":"minor"},
|
| 6 |
+
{"id":"P005","category":"missing_period","input":"هذا الكتاب مفيد جداً وأنصح بقراءته","should_add_punct":true,"severity":"minor"},
|
| 7 |
+
{"id":"P006","category":"missing_period","input":"العلم نور والجهل ظلام والتعليم مهم","should_add_punct":true,"severity":"minor"},
|
| 8 |
+
{"id":"P007","category":"missing_question","input":"ماذا تريد أن تفعل اليوم","should_add_punct":true,"severity":"minor"},
|
| 9 |
+
{"id":"P008","category":"missing_question","input":"لماذا لم تحضر أمس","should_add_punct":true,"severity":"minor"},
|
| 10 |
+
{"id":"P009","category":"missing_comma","input":"جاء أحمد ومحمد وعلي","should_add_punct":true,"severity":"minor"},
|
| 11 |
+
{"id":"P010","category":"missing_multi","input":"ذهبت إلى السوق واشتريت خبزاً ولحماً ثم عدت","should_add_punct":true,"severity":"minor"},
|
| 12 |
+
{"id":"P011","category":"already_correct","input":"ذهبت إلى المدرسة. ثم عدت.","should_add_punct":false,"severity":"major"},
|
| 13 |
+
{"id":"P012","category":"already_correct","input":"كيف حالك؟ أنا بخير.","should_add_punct":false,"severity":"major"},
|
| 14 |
+
{"id":"P013","category":"already_correct","input":"أحمد، كيف حالك؟ هل أنت بخير؟","should_add_punct":false,"severity":"major"},
|
| 15 |
+
{"id":"P014","category":"already_correct","input":"قال: أنا بخير، شكراً لك.","should_add_punct":false,"severity":"major"},
|
| 16 |
+
{"id":"P015","category":"already_correct","input":"ذهبت إلى المكتبة، واشتريت كتاباً.","should_add_punct":false,"severity":"major"},
|
| 17 |
+
{"id":"P016","category":"word_preservation","input":"ذهبت الي المدرسه أمس","expected_words_unchanged":true,"severity":"critical"},
|
| 18 |
+
{"id":"P017","category":"word_preservation","input":"انا طالب في الجامعه","expected_words_unchanged":true,"severity":"critical"},
|
| 19 |
+
{"id":"P018","category":"dialogue","input":"قال أحمد أنا سعيد بلقائك يا صديقي","should_add_punct":true,"severity":"minor"},
|
| 20 |
+
{"id":"P019","category":"enumeration","input":"أحتاج إلى خبز ولبن وجبن وبيض","should_add_punct":true,"severity":"minor"},
|
| 21 |
+
{"id":"P020","category":"exclamation","input":"يا إلهي هذا رائع جداً","should_add_punct":true,"severity":"minor"}
|
| 22 |
+
]
|
tests/phase10/gold_datasets/religious.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{"id":"R001","category":"basmalah","input":"بسم الله الرحمن الرحيم","must_preserve":true,"severity":"critical"},
|
| 3 |
+
{"id":"R002","category":"fatiha","input":"الحمد لله رب العالمين الرحمن الرحيم مالك يوم الدين","must_preserve":true,"severity":"critical"},
|
| 4 |
+
{"id":"R003","category":"fatiha","input":"إياك نعبد وإياك نستعين","must_preserve":true,"severity":"critical"},
|
| 5 |
+
{"id":"R004","category":"fatiha","input":"اهدنا الصراط المستقيم صراط الذين أنعمت عليهم","must_preserve":true,"severity":"critical"},
|
| 6 |
+
{"id":"R005","category":"ikhlas","input":"قل هو الله أحد الله الصمد لم يلد ولم يولد","must_preserve":true,"severity":"critical"},
|
| 7 |
+
{"id":"R006","category":"qadr","input":"إنا أنزلناه في ليلة القدر","must_preserve":true,"severity":"critical"},
|
| 8 |
+
{"id":"R007","category":"falaq","input":"قل أعوذ برب الفلق من شر ما خلق","must_preserve":true,"severity":"critical"},
|
| 9 |
+
{"id":"R008","category":"nas","input":"قل أعوذ برب الناس ملك الناس إله الناس","must_preserve":true,"severity":"critical"},
|
| 10 |
+
{"id":"R009","category":"baqara","input":"ذلك الكتاب لا ريب فيه هدى للمتقين","must_preserve":true,"severity":"critical"},
|
| 11 |
+
{"id":"R010","category":"baqara","input":"الذين يؤمنون بالغيب ويقيمون الصلاة","must_preserve":true,"severity":"critical"},
|
| 12 |
+
{"id":"R011","category":"kursi","input":"الله لا إله إلا هو الحي القيوم لا تأخذه سنة ولا نوم","must_preserve":true,"severity":"critical"},
|
| 13 |
+
{"id":"R012","category":"shahada","input":"لا إله إلا الله محمد رسول الله","must_preserve":true,"severity":"critical"},
|
| 14 |
+
{"id":"R013","category":"shahada","input":"أشهد أن لا إله إلا الله وأشهد أن محمداً رسول الله","must_preserve":true,"severity":"critical"},
|
| 15 |
+
{"id":"R014","category":"hadith","input":"إنما الأعمال بالنيات وإنما لكل امرئ ما نوى","must_preserve":true,"severity":"critical"},
|
| 16 |
+
{"id":"R015","category":"hadith","input":"خيركم من تعلم القرآن وعلمه","must_preserve":true,"severity":"critical"},
|
| 17 |
+
{"id":"R016","category":"hadith","input":"المسلم من سلم المسلمون من لسانه ويده","must_preserve":true,"severity":"critical"},
|
| 18 |
+
{"id":"R017","category":"hadith","input":"لا يؤمن أحدكم حتى يحب لأخيه ما يحب لنفسه","must_preserve":true,"severity":"critical"},
|
| 19 |
+
{"id":"R018","category":"hadith","input":"من كان يؤمن بالله واليوم الآخر فليقل خيراً أو ليصمت","must_preserve":true,"severity":"critical"},
|
| 20 |
+
{"id":"R019","category":"dua","input":"ربنا آتنا في الدنيا حسنة وفي الآخرة حسنة وقنا عذاب النار","must_preserve":true,"severity":"critical"},
|
| 21 |
+
{"id":"R020","category":"dua","input":"رب اشرح لي صدري ويسر لي أمري","must_preserve":true,"severity":"critical"},
|
| 22 |
+
{"id":"R021","category":"dua","input":"حسبنا الله ونعم الوكيل","must_preserve":true,"severity":"critical"},
|
| 23 |
+
{"id":"R022","category":"dua","input":"لا حول ولا قوة إلا بالله","must_preserve":true,"severity":"critical"},
|
| 24 |
+
{"id":"R023","category":"hamdalah","input":"الحمد لله رب العالمين","must_preserve":true,"severity":"critical"},
|
| 25 |
+
{"id":"R024","category":"tasbih","input":"سبحان الله وبحمده سبحان الله العظيم","must_preserve":true,"severity":"critical"},
|
| 26 |
+
{"id":"R025","category":"salawat","input":"اللهم صل وسلم على نبينا محمد","must_preserve":true,"severity":"critical"},
|
| 27 |
+
{"id":"R026","category":"istighfar","input":"أستغفر الله العظيم وأتوب إليه","must_preserve":true,"severity":"critical"},
|
| 28 |
+
{"id":"R027","category":"takbir","input":"الله أكبر الله أكبر لا إله إلا الله","must_preserve":true,"severity":"critical"},
|
| 29 |
+
{"id":"R028","category":"inna","input":"إنا لله وإنا إليه راجعون","must_preserve":true,"severity":"critical"},
|
| 30 |
+
{"id":"R029","category":"bismillah","input":"بسم الله والحمد لله","must_preserve":true,"severity":"critical"},
|
| 31 |
+
{"id":"R030","category":"salam","input":"السلام عليكم ورحمة الله وبركاته","must_preserve":true,"severity":"critical"}
|
| 32 |
+
]
|
tests/phase10/gold_datasets/spelling.json
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{"id":"S001","category":"hamza","input":"انا طالب في الجامعة","expected":"أنا طالب في الجامعة","error_words":["انا"],"severity":"major"},
|
| 3 |
+
{"id":"S002","category":"hamza","input":"اذا جاء الربيع تزهر الأشجار","expected":"إذا جاء الربيع تزهر الأشجار","error_words":["اذا"],"severity":"major"},
|
| 4 |
+
{"id":"S003","category":"hamza","input":"ايضا هذا الأمر مهم جداً","expected":"أيضاً هذا الأمر مهم جداً","error_words":["ايضا"],"severity":"major"},
|
| 5 |
+
{"id":"S004","category":"hamza","input":"لان الأمر يتعلق بالمستقبل","expected":"لأن الأمر يتعلق بالمستقبل","error_words":["لان"],"severity":"major"},
|
| 6 |
+
{"id":"S005","category":"hamza","input":"اين ذهبت أمس","expected":"أين ذهبت أمس","error_words":["اين"],"severity":"major"},
|
| 7 |
+
{"id":"S006","category":"hamza","input":"اول مرة أزور هذا المكان","expected":"أول مرة أزور هذا المكان","error_words":["اول"],"severity":"major"},
|
| 8 |
+
{"id":"S007","category":"hamza","input":"هذا او ذاك لا فرق","expected":"هذا أو ذاك لا فرق","error_words":["او"],"severity":"major"},
|
| 9 |
+
{"id":"S008","category":"hamza","input":"اكبر مدينة في العالم","expected":"أكبر مدينة في العالم","error_words":["اكبر"],"severity":"major"},
|
| 10 |
+
{"id":"S009","category":"hamza","input":"اصغر طالب في الصف","expected":"أصغر طالب في الصف","error_words":["اصغر"],"severity":"major"},
|
| 11 |
+
{"id":"S010","category":"hamza","input":"ابناء الوطن يعملون بجد","expected":"أبناء الوطن يعملون بجد","error_words":["ابناء"],"severity":"major"},
|
| 12 |
+
{"id":"S011","category":"hamza","input":"اطفال المدرسة يلعبون","expected":"أطفال المدرسة يلعبون","error_words":["اطفال"],"severity":"major"},
|
| 13 |
+
{"id":"S012","category":"hamza","input":"اخيراً وصلنا إلى الهدف","expected":"أخيراً وصلنا إلى الهدف","error_words":["اخيراً"],"severity":"major"},
|
| 14 |
+
{"id":"S013","category":"hamza","input":"وقف امام المدرسة","expected":"وقف أمام المدرسة","error_words":["امام"],"severity":"major"},
|
| 15 |
+
{"id":"S014","category":"hamza","input":"انت طالب مجتهد","expected":"أنت طالب مجتهد","error_words":["انت"],"severity":"major"},
|
| 16 |
+
{"id":"S015","category":"hamza","input":"اننا نحب الوطن","expected":"إننا نحب الوطن","error_words":["اننا"],"severity":"major"},
|
| 17 |
+
{"id":"S016","category":"hamza","input":"ان شاء الله سنفوز","expected":"إن شاء الله سنفوز","error_words":["ان"],"severity":"major"},
|
| 18 |
+
{"id":"S017","category":"hamza","input":"اقترب الموعد النهائي","expected":"اقترب الموعد النهائي","error_words":[],"severity":"none"},
|
| 19 |
+
{"id":"S018","category":"hamza","input":"ارسل الرسالة فوراً","expected":"أرسل الرسالة فوراً","error_words":["ارسل"],"severity":"major"},
|
| 20 |
+
{"id":"S019","category":"hamza","input":"اخر الأخبار في المساء","expected":"آخر الأخبار في المساء","error_words":["اخر"],"severity":"major"},
|
| 21 |
+
{"id":"S020","category":"hamza","input":"اسرع في المشي قليلاً","expected":"أسرع في المشي قليلاً","error_words":["اسرع"],"severity":"major"},
|
| 22 |
+
{"id":"S021","category":"hamza","input":"اجمل مكان في العالم","expected":"أجمل مكان في العالم","error_words":["اجمل"],"severity":"major"},
|
| 23 |
+
{"id":"S022","category":"hamza","input":"اعلن الرئيس عن القرار","expected":"أعلن الرئيس عن القرار","error_words":["اعلن"],"severity":"major"},
|
| 24 |
+
{"id":"S023","category":"hamza","input":"اكد الوزير على الخطة","expected":"أكد الوزير على الخطة","error_words":["اكد"],"severity":"major"},
|
| 25 |
+
{"id":"S024","category":"hamza","input":"اشار التقرير إلى ذلك","expected":"أشار التقرير إلى ذلك","error_words":["اشار"],"severity":"major"},
|
| 26 |
+
{"id":"S025","category":"hamza","input":"اعتقد انه سيحضر غداً","expected":"أعتقد أنه سيحضر غداً","error_words":["اعتقد","انه"],"severity":"major"},
|
| 27 |
+
{"id":"S026","category":"hamza_prefix","input":"والاسعار مرتفعة جداً","expected":"والأسعار مرتفعة جداً","error_words":["والاسعار"],"severity":"major"},
|
| 28 |
+
{"id":"S027","category":"hamza_prefix","input":"بالاضافة إلى ذلك","expected":"بالإضافة إلى ذلك","error_words":["بالاضافة"],"severity":"major"},
|
| 29 |
+
{"id":"S028","category":"hamza_prefix","input":"فالانسان يحتاج للعلم","expected":"فالإنسان يحتاج للعلم","error_words":["فالانسان"],"severity":"major"},
|
| 30 |
+
{"id":"S029","category":"hamza_prefix","input":"كالاطفال في اللعب","expected":"كالأطفال في اللعب","error_words":["كالاطفال"],"severity":"major"},
|
| 31 |
+
{"id":"S030","category":"hamza_prefix","input":"للاسف لم ينجح","expected":"للأسف لم ينجح","error_words":["للاسف"],"severity":"major"},
|
| 32 |
+
{"id":"S031","category":"ta_marbuta","input":"المدرسه كبيره وجميله","expected":"المدرسة كبيرة وجميلة","error_words":["المدرسه","كبيره","جميله"],"severity":"major"},
|
| 33 |
+
{"id":"S032","category":"ta_marbuta","input":"الجامعه في القاهره","expected":"الجامعة في القاهرة","error_words":["الجامعه","القاهره"],"severity":"major"},
|
| 34 |
+
{"id":"S033","category":"ta_marbuta","input":"السياره سريعه جداً","expected":"السيارة سريعة جداً","error_words":["السياره","سريعه"],"severity":"major"},
|
| 35 |
+
{"id":"S034","category":"ta_marbuta","input":"الشجره طويله وجميله","expected":"الشجرة طويلة وجميلة","error_words":["الشجره","طويله","جميله"],"severity":"major"},
|
| 36 |
+
{"id":"S035","category":"ta_marbuta","input":"الحياه صعبه في المدينه","expected":"الحياة صعبة في المدينة","error_words":["الحياه","صعبه","المدينه"],"severity":"major"},
|
| 37 |
+
{"id":"S036","category":"ta_marbuta","input":"المكتبه قريبه من البيت","expected":"المكتبة قريبة من البيت","error_words":["المكتبه","قريبه"],"severity":"major"},
|
| 38 |
+
{"id":"S037","category":"ta_marbuta","input":"الغرفه نظيفه ومرتبه","expected":"الغرفة نظيفة ومرتبة","error_words":["الغرفه","نظيفه","مرتبه"],"severity":"major"},
|
| 39 |
+
{"id":"S038","category":"ta_marbuta","input":"القصه مثيره للاهتمام","expected":"القصة مثيرة للاهتمام","error_words":["القصه","مثيره"],"severity":"major"},
|
| 40 |
+
{"id":"S039","category":"ta_marbuta","input":"الرحله طويله ومتعبه","expected":"الرحلة طويلة ومتعبة","error_words":["الرحله","طويله","متعبه"],"severity":"major"},
|
| 41 |
+
{"id":"S040","category":"ta_marbuta","input":"الوظيفه صعبه لكنها مفيده","expected":"الوظيفة صعبة لكنها مفيدة","error_words":["الوظيفه","صعبه","مفيده"],"severity":"major"},
|
| 42 |
+
{"id":"S041","category":"ta_marbuta_prefix","input":"بالمدرسه الكبيره","expected":"بالمدرسة الكبيرة","error_words":["بالمدرسه","الكبيره"],"severity":"major"},
|
| 43 |
+
{"id":"S042","category":"ta_marbuta_prefix","input":"والجامعه المصريه","expected":"والجامعة المصرية","error_words":["والجامعه","المصريه"],"severity":"major"},
|
| 44 |
+
{"id":"S043","category":"ta_marbuta_prefix","input":"فالسياره الجديده","expected":"فالسيارة الجديدة","error_words":["فالسياره","الجديده"],"severity":"major"},
|
| 45 |
+
{"id":"S044","category":"ta_marbuta_prefix","input":"كالمدرسه القديمه","expected":"كالمدرسة القديمة","error_words":["كالمدرسه","القديمه"],"severity":"major"},
|
| 46 |
+
{"id":"S045","category":"ta_marbuta_prefix","input":"للمدرسه الابتدائيه","expected":"للمدرسة الابتدائية","error_words":["للمدرسه","الابتدائيه"],"severity":"major"},
|
| 47 |
+
{"id":"S046","category":"alif_maqsura","input":"ذهبت الي المكتبة","expected":"ذهبت إلى المكتبة","error_words":["الي"],"severity":"major"},
|
| 48 |
+
{"id":"S047","category":"alif_maqsura","input":"المستشفي الكبير","expected":"المستشفى الكبير","error_words":["المستشفي"],"severity":"major"},
|
| 49 |
+
{"id":"S048","category":"alif_maqsura","input":"هدي الطالبة ممتاز","expected":"هدى الطالبة ممتاز","error_words":["هدي"],"severity":"major"},
|
| 50 |
+
{"id":"S049","category":"alif_maqsura","input":"مصطفي طالب مجتهد","expected":"مصطفى طالب مجتهد","error_words":["مصطفي"],"severity":"major"},
|
| 51 |
+
{"id":"S050","category":"alif_maqsura","input":"موسي نبي عظيم","expected":"موسى نبي عظيم","error_words":["موسي"],"severity":"major"},
|
| 52 |
+
{"id":"S051","category":"alif_maqsura","input":"علي يدرس في الكلية","expected":"علي يدرس في الكلية","error_words":[],"severity":"none"},
|
| 53 |
+
{"id":"S052","category":"alif_maqsura","input":"ذهب الي السوق","expected":"ذهب إلى السوق","error_words":["الي"],"severity":"major"},
|
| 54 |
+
{"id":"S053","category":"alif_maqsura","input":"بني المبنى الجديد","expected":"بنى المبنى الجديد","error_words":["بني"],"severity":"major"},
|
| 55 |
+
{"id":"S054","category":"word_split","input":"ذهبت فيالبيت","expected":"ذهبت في البيت","error_words":["فيالبيت"],"severity":"major"},
|
| 56 |
+
{"id":"S055","category":"word_split","input":"خرج ��نالمدرسة","expected":"خرج من المدرسة","error_words":["منالمدرسة"],"severity":"major"},
|
| 57 |
+
{"id":"S056","category":"word_split","input":"بقي عندالباب","expected":"بقي عند الباب","error_words":["عندالباب"],"severity":"major"},
|
| 58 |
+
{"id":"S057","category":"word_split","input":"جلس عندالنافذة","expected":"جلس عند النافذة","error_words":["عندالنافذة"],"severity":"major"},
|
| 59 |
+
{"id":"S058","category":"word_split","input":"رجع الىالبيت","expected":"رجع إلى البيت","error_words":["الىالبيت"],"severity":"major"},
|
| 60 |
+
{"id":"S059","category":"word_split","input":"خرج منالباب الخلفي","expected":"خرج من الباب الخلفي","error_words":["منالباب"],"severity":"major"},
|
| 61 |
+
{"id":"S060","category":"word_split","input":"نظر الىالسماء","expected":"نظر إلى السماء","error_words":["الىالسماء"],"severity":"major"},
|
| 62 |
+
{"id":"S061","category":"correct_text","input":"أنا ذهبت إلى الجامعة","expected":"أنا ذهبت إلى الجامعة","error_words":[],"severity":"none"},
|
| 63 |
+
{"id":"S062","category":"correct_text","input":"هذه المدرسة جميلة جداً","expected":"هذه المدرسة جميلة جداً","error_words":[],"severity":"none"},
|
| 64 |
+
{"id":"S063","category":"correct_text","input":"كان الجو ممطراً اليوم","expected":"كان الجو ممطراً اليوم","error_words":[],"severity":"none"},
|
| 65 |
+
{"id":"S064","category":"correct_text","input":"وكان أحمد في المنزل","expected":"وكان أحمد في المنزل","error_words":[],"severity":"none"},
|
| 66 |
+
{"id":"S065","category":"correct_text","input":"إلى اللقاء يا صديقي","expected":"إلى اللقاء يا صديقي","error_words":[],"severity":"none"},
|
| 67 |
+
{"id":"S066","category":"correct_text","input":"ذلك الكتاب مفيد جداً","expected":"ذلك الكتاب مفيد جداً","error_words":[],"severity":"none"},
|
| 68 |
+
{"id":"S067","category":"correct_text","input":"لكن الأمر صعب علينا","expected":"لكن الأمر صعب علينا","error_words":[],"severity":"none"},
|
| 69 |
+
{"id":"S068","category":"correct_text","input":"هذا أو ذاك سواء عندي","expected":"هذا أو ذاك سواء عندي","error_words":[],"severity":"none"},
|
| 70 |
+
{"id":"S069","category":"correct_text","input":"الطالب المجتهد ينجح دائماً","expected":"الطالب المجتهد ينجح دائماً","error_words":[],"severity":"none"},
|
| 71 |
+
{"id":"S070","category":"correct_text","input":"العلم نور والجهل ظلام","expected":"العلم نور والجهل ظلام","error_words":[],"severity":"none"},
|
| 72 |
+
{"id":"S071","category":"correct_text","input":"أحب القراءة والكتابة","expected":"أحب القراءة والكتابة","error_words":[],"severity":"none"},
|
| 73 |
+
{"id":"S072","category":"correct_text","input":"المعلم يشرح الدرس","expected":"المعلم يشرح الدرس","error_words":[],"severity":"none"},
|
| 74 |
+
{"id":"S073","category":"correct_text","input":"ذهبت إلى السوق واشتريت خبزاً","expected":"ذهبت إلى السوق واشتريت خبزاً","error_words":[],"severity":"none"},
|
| 75 |
+
{"id":"S074","category":"correct_text","input":"الطقس جميل في الربيع","expected":"الطقس جميل في الربيع","error_words":[],"severity":"none"},
|
| 76 |
+
{"id":"S075","category":"correct_text","input":"نحن نعمل بجد كل يوم","expected":"نحن نعمل بجد كل يوم","error_words":[],"severity":"none"},
|
| 77 |
+
{"id":"S076","category":"multi_error","input":"انا ذهبت الي المدرسه","expected":"أنا ذهبت إلى المدرسة","error_words":["انا","الي","المدرسه"],"severity":"critical"},
|
| 78 |
+
{"id":"S077","category":"multi_error","input":"ايضا الجامعه كبيره","expected":"أيضاً الجامعة كبيرة","error_words":["ايضا","الجامعه","كبيره"],"severity":"critical"},
|
| 79 |
+
{"id":"S078","category":"multi_error","input":"لان المدرسه بعيده جداً","expected":"لأن المدرسة بعيدة جداً","error_words":["لان","المدرسه","بعيده"],"severity":"critical"},
|
| 80 |
+
{"id":"S079","category":"multi_error","input":"اين الجامعه الكبيره","expected":"أين الجامعة الكبيرة","error_words":["اين","الجامعه","الكبيره"],"severity":"critical"},
|
| 81 |
+
{"id":"S080","category":"multi_error","input":"اول مره ازور المكتبه","expected":"أول مرة أزور المكتبة","error_words":["اول","مره","ازور","المكتبه"],"severity":"critical"}
|
| 82 |
+
]
|
tests/phase10/gold_datasets/structured_content.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{"id":"SC001","category":"url","input":"زر الموقع https://example.com للمزيد","protected":"https://example.com","severity":"critical"},
|
| 3 |
+
{"id":"SC002","category":"url","input":"الرابط https://www.google.com/search?q=test يعمل","protected":"https://www.google.com/search?q=test","severity":"critical"},
|
| 4 |
+
{"id":"SC003","category":"url","input":"موقع http://localhost:8080/api جاهز","protected":"http://localhost:8080/api","severity":"critical"},
|
| 5 |
+
{"id":"SC004","category":"url","input":"اقرأ المقال على https://medium.com/@user/article-title","protected":"https://medium.com/@user/article-title","severity":"critical"},
|
| 6 |
+
{"id":"SC005","category":"email","input":"أرسل لي على info@company.com","protected":"info@company.com","severity":"critical"},
|
| 7 |
+
{"id":"SC006","category":"email","input":"بريدي user.name@gmail.com للتواصل","protected":"user.name@gmail.com","severity":"critical"},
|
| 8 |
+
{"id":"SC007","category":"email","input":"تواصل عبر support@bayan.ai","protected":"support@bayan.ai","severity":"critical"},
|
| 9 |
+
{"id":"SC008","category":"date","input":"تاريخ اليوم 15/06/2026","protected":"15/06/2026","severity":"major"},
|
| 10 |
+
{"id":"SC009","category":"date","input":"الموعد يوم 2026-06-22","protected":"2026-06-22","severity":"major"},
|
| 11 |
+
{"id":"SC010","category":"date","input":"في تاريخ 01/01/2025 بدأنا","protected":"01/01/2025","severity":"major"},
|
| 12 |
+
{"id":"SC011","category":"time","input":"الساعة 14:30 عصراً","protected":"14:30","severity":"major"},
|
| 13 |
+
{"id":"SC012","category":"time","input":"الموعد الساعة 3:30 مساءً","protected":"3:30","severity":"major"},
|
| 14 |
+
{"id":"SC013","category":"time","input":"يبدأ الاجتماع 09:00 صباحاً","protected":"09:00","severity":"major"},
|
| 15 |
+
{"id":"SC014","category":"number","input":"المسافة 25.5 كيلومتر","protected":"25.5","severity":"major"},
|
| 16 |
+
{"id":"SC015","category":"number","input":"السعر 1,000,000 جنيه","protected":"1,000,000","severity":"major"},
|
| 17 |
+
{"id":"SC016","category":"number","input":"النسبة 95.7% من الطلاب نجحوا","protected":"95.7%","severity":"major"},
|
| 18 |
+
{"id":"SC017","category":"currency","input":"الثمن 500$ أمريكي","protected":"500$","severity":"major"},
|
| 19 |
+
{"id":"SC018","category":"currency","input":"الميزانية 100 جنيه مصري","protected":"100","severity":"minor"},
|
| 20 |
+
{"id":"SC019","category":"measurement","input":"الحرارة 35°C اليوم","protected":"35°C","severity":"major"},
|
| 21 |
+
{"id":"SC020","category":"measurement","input":"المسافة 25km تقريباً","protected":"25km","severity":"major"},
|
| 22 |
+
{"id":"SC021","category":"measurement","input":"الوزن 75kg","protected":"75kg","severity":"major"},
|
| 23 |
+
{"id":"SC022","category":"code","input":"استخدم print('مرحبا') للطباعة","protected":"print('مرحبا')","severity":"critical"},
|
| 24 |
+
{"id":"SC023","category":"code","input":"المتغير const x = 5; في جافاسكريبت","protected":"const x = 5;","severity":"critical"},
|
| 25 |
+
{"id":"SC024","category":"code","input":"الدالة function test() {} تعمل","protected":"function test() {}","severity":"critical"},
|
| 26 |
+
{"id":"SC025","category":"sql","input":"الاستعلام SELECT * FROM users يعمل","protected":"SELECT * FROM users","severity":"critical"},
|
| 27 |
+
{"id":"SC026","category":"json","input":"البيانات {\"name\":\"Mohamed\"} صحيحة","protected":"{\"name\":\"Mohamed\"}","severity":"critical"},
|
| 28 |
+
{"id":"SC027","category":"hashtag","input":"مشروع #بيان رائع جداً","protected":"#بيان","severity":"major"},
|
| 29 |
+
{"id":"SC028","category":"hashtag","input":"هاشتاق #الذكاء_الاصطناعي مهم","protected":"#الذكاء_الاصطناعي","severity":"major"},
|
| 30 |
+
{"id":"SC029","category":"mention","input":"شكراً @mohamedatef على المساعدة","protected":"@mohamedatef","severity":"major"},
|
| 31 |
+
{"id":"SC030","category":"mention","input":"تابع @bayan_app للتحديثات","protected":"@bayan_app","severity":"major"},
|
| 32 |
+
{"id":"SC031","category":"phone","input":"اتصل على 01012345678 للاستفسار","protected":"01012345678","severity":"major"},
|
| 33 |
+
{"id":"SC032","category":"phone","input":"الرقم +201012345678 متاح","protected":"+201012345678","severity":"major"},
|
| 34 |
+
{"id":"SC033","category":"ip","input":"الخادم على 192.168.1.1 يعمل","protected":"192.168.1.1","severity":"major"},
|
| 35 |
+
{"id":"SC034","category":"version","input":"الإصدار v2.1.0 متاح","protected":"v2.1.0","severity":"minor"},
|
| 36 |
+
{"id":"SC035","category":"filepath","input":"الملف في C:\\Users\\test\\file.txt","protected":"C:\\Users\\test\\file.txt","severity":"major"}
|
| 37 |
+
]
|
tests/phase10/reports/Phase10_Benchmark_Report.md
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Phase 10 — Benchmark Report
|
| 2 |
+
|
| 3 |
+
> **Date**: 2026-06-22 | **Target**: Production API | **Total Tests**: 270
|
| 4 |
+
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
## 1. Aggregate Metrics
|
| 8 |
+
|
| 9 |
+
| Metric | Value |
|
| 10 |
+
|---|---|
|
| 11 |
+
| **Total Tests** | 270 |
|
| 12 |
+
| **Pass Rate** | **25.6%** |
|
| 13 |
+
| **Precision** | 0.277 |
|
| 14 |
+
| **Recall** | 0.557 |
|
| 15 |
+
| **F1** | 0.370 |
|
| 16 |
+
| **FPR** | 0.939 |
|
| 17 |
+
| **FNR** | 0.443 |
|
| 18 |
+
| **Overcorrection Rate** | 57.0% |
|
| 19 |
+
| **Undercorrection Rate** | 17.4% |
|
| 20 |
+
| **Latency p50** | 4,162 ms |
|
| 21 |
+
| **Latency p95** | 12,942 ms |
|
| 22 |
+
|
| 23 |
+
| Verdict | Count | % |
|
| 24 |
+
|---|---|---|
|
| 25 |
+
| TP (correct fix) | 59 | 21.9% |
|
| 26 |
+
| TN (correct no-change) | 10 | 3.7% |
|
| 27 |
+
| FP (overcorrection) | **154** | **57.0%** |
|
| 28 |
+
| FN (undercorrection) | 47 | 17.4% |
|
| 29 |
+
| ERROR | 0 | 0% |
|
| 30 |
+
|
| 31 |
+
---
|
| 32 |
+
|
| 33 |
+
## 2. Per-Dataset Metrics
|
| 34 |
+
|
| 35 |
+
### 2.1 Spelling (80 tests)
|
| 36 |
+
|
| 37 |
+
| Metric | Value |
|
| 38 |
+
|---|---|
|
| 39 |
+
| Pass Rate | 42.5% |
|
| 40 |
+
| Precision | 0.667 |
|
| 41 |
+
| Recall | 0.540 |
|
| 42 |
+
| F1 | 0.597 |
|
| 43 |
+
| FPR | 1.000 |
|
| 44 |
+
| FNR | 0.460 |
|
| 45 |
+
| Latency p50 | 3,080 ms |
|
| 46 |
+
| Latency p95 | 5,352 ms |
|
| 47 |
+
|
| 48 |
+
**Breakdown by category:**
|
| 49 |
+
|
| 50 |
+
| Category | TP | FP | TN | FN |
|
| 51 |
+
|---|---|---|---|---|
|
| 52 |
+
| hamza | 13 | 0 | 0 | 12 |
|
| 53 |
+
| hamza_prefix | 2 | 0 | 0 | 3 |
|
| 54 |
+
| ta_marbuta | 8 | 0 | 0 | 2 |
|
| 55 |
+
| ta_marbuta_prefix | 3 | 0 | 0 | 2 |
|
| 56 |
+
| alif_maqsura | 3 | 0 | 0 | 5 |
|
| 57 |
+
| word_split | 0 | 0 | 0 | 7 |
|
| 58 |
+
| correct_text | 0 | 15 | 0 | 0 |
|
| 59 |
+
| multi_error | 5 | 2 | 0 | 0 |
|
| 60 |
+
|
| 61 |
+
> Key: Spelling model misses 48% of errors, and ALL correct text gets punctuation added (100% FPR on correct text).
|
| 62 |
+
|
| 63 |
+
---
|
| 64 |
+
|
| 65 |
+
### 2.2 Grammar (45 tests)
|
| 66 |
+
|
| 67 |
+
| Metric | Value |
|
| 68 |
+
|---|---|
|
| 69 |
+
| Pass Rate | 26.7% |
|
| 70 |
+
| Precision | 0.444 |
|
| 71 |
+
| Recall | 0.400 |
|
| 72 |
+
| F1 | 0.421 |
|
| 73 |
+
| FPR | 1.000 |
|
| 74 |
+
| FNR | 0.600 |
|
| 75 |
+
| Latency p50 | 3,263 ms |
|
| 76 |
+
| Latency p95 | 4,524 ms |
|
| 77 |
+
|
| 78 |
+
**Breakdown by category:**
|
| 79 |
+
|
| 80 |
+
| Category | TP | FP | TN | FN |
|
| 81 |
+
|---|---|---|---|---|
|
| 82 |
+
| sv_agree | 0 | 0 | 0 | 10 |
|
| 83 |
+
| gender | 5 | 0 | 0 | 0 |
|
| 84 |
+
| case | 0 | 0 | 0 | 5 |
|
| 85 |
+
| five_nouns | 2 | 0 | 0 | 2 |
|
| 86 |
+
| dual | 2 | 0 | 0 | 0 |
|
| 87 |
+
| nasb | 3 | 0 | 0 | 1 |
|
| 88 |
+
| correct | 0 | 15 | 0 | 0 |
|
| 89 |
+
|
| 90 |
+
> Key: **100% failure on SV agreement** (0/10) and **100% failure on case endings** (0/5). All 15 correct grammar sentences got punctuation added.
|
| 91 |
+
|
| 92 |
+
---
|
| 93 |
+
|
| 94 |
+
### 2.3 Punctuation (20 tests)
|
| 95 |
+
|
| 96 |
+
| Metric | Value |
|
| 97 |
+
|---|---|
|
| 98 |
+
| Pass Rate | **80.0%** |
|
| 99 |
+
| Precision | 0.765 |
|
| 100 |
+
| Recall | **1.000** |
|
| 101 |
+
| F1 | 0.867 |
|
| 102 |
+
| FPR | 0.571 |
|
| 103 |
+
| FNR | 0.000 |
|
| 104 |
+
| Latency p50 | 5,119 ms |
|
| 105 |
+
| Latency p95 | 9,531 ms |
|
| 106 |
+
|
| 107 |
+
> Key: Best performing model. Perfect recall but over-punctuates already-correct text (4/7 correct samples modified).
|
| 108 |
+
|
| 109 |
+
---
|
| 110 |
+
|
| 111 |
+
### 2.4 Entities (30 tests)
|
| 112 |
+
|
| 113 |
+
| Metric | Value |
|
| 114 |
+
|---|---|
|
| 115 |
+
| Pass Rate | **6.7%** |
|
| 116 |
+
| Overcorrection Rate | **93.3%** |
|
| 117 |
+
| Latency p50 | 4,076 ms |
|
| 118 |
+
|
| 119 |
+
> Key: 28/30 entity contexts modified. Primary cause: punctuation adding periods, not actual entity corruption.
|
| 120 |
+
|
| 121 |
+
---
|
| 122 |
+
|
| 123 |
+
### 2.5 Religious (30 tests)
|
| 124 |
+
|
| 125 |
+
| Metric | Value |
|
| 126 |
+
|---|---|
|
| 127 |
+
| Pass Rate | **10.0%** |
|
| 128 |
+
| Modification Rate | **90.0%** |
|
| 129 |
+
| Latency p50 | 5,863 ms |
|
| 130 |
+
| Latency p95 | 13,356 ms |
|
| 131 |
+
|
| 132 |
+
> Key: Only 3/30 religious texts preserved (Al-Fatiha L2, Ayat al-Kursi, Takbir).
|
| 133 |
+
|
| 134 |
+
---
|
| 135 |
+
|
| 136 |
+
### 2.6 Structured Content (35 tests)
|
| 137 |
+
|
| 138 |
+
| Metric | Value |
|
| 139 |
+
|---|---|
|
| 140 |
+
| Pass Rate | **5.7%** |
|
| 141 |
+
| Corruption Rate | **94.3%** |
|
| 142 |
+
| Latency p50 | 7,652 ms |
|
| 143 |
+
| Latency p95 | 14,014 ms |
|
| 144 |
+
|
| 145 |
+
> Key: Only 2/35 structured content samples preserved (one URL returned before API loaded, one filepath).
|
| 146 |
+
|
| 147 |
+
---
|
| 148 |
+
|
| 149 |
+
### 2.7 Hallucination (30 tests)
|
| 150 |
+
|
| 151 |
+
| Metric | Value |
|
| 152 |
+
|---|---|
|
| 153 |
+
| Pass Rate | **0.0%** |
|
| 154 |
+
| Hallucination Rate | **100%** |
|
| 155 |
+
| Latency p50 | 11,141 ms |
|
| 156 |
+
| Latency p95 | 15,728 ms |
|
| 157 |
+
|
| 158 |
+
> [!CAUTION]
|
| 159 |
+
> **100% hallucination rate.** Every single correctly-written text was modified. The system cannot distinguish correct text from incorrect text.
|
| 160 |
+
|
| 161 |
+
---
|
| 162 |
+
|
| 163 |
+
## 3. Latency Analysis
|
| 164 |
+
|
| 165 |
+
| Dataset | p50 | p95 | Avg Text Length |
|
| 166 |
+
|---|---|---|---|
|
| 167 |
+
| Spelling | 3,080 ms | 5,352 ms | ~25 chars |
|
| 168 |
+
| Grammar | 3,263 ms | 4,524 ms | ~30 chars |
|
| 169 |
+
| Punctuation | 5,119 ms | 9,531 ms | ~40 chars |
|
| 170 |
+
| Entities | 4,076 ms | 6,719 ms | ~30 chars |
|
| 171 |
+
| Religious | 5,863 ms | 13,356 ms | ~50 chars |
|
| 172 |
+
| Structured | 7,652 ms | 14,014 ms | ~40 chars |
|
| 173 |
+
| Hallucination | 11,141 ms | 15,728 ms | ~70 chars |
|
| 174 |
+
|
| 175 |
+
**Degradation curve**: Latency scales roughly linearly with text length, with ~150ms per character for longer texts.
|
| 176 |
+
|
| 177 |
+
---
|
| 178 |
+
|
| 179 |
+
## 4. Span Alignment
|
| 180 |
+
|
| 181 |
+
| Metric | Value |
|
| 182 |
+
|---|---|
|
| 183 |
+
| Total span checks | 270 |
|
| 184 |
+
| Span errors | **0** |
|
| 185 |
+
| Span validity rate | **100%** |
|
| 186 |
+
|
| 187 |
+
✅ All spans correctly aligned.
|
| 188 |
+
|
| 189 |
+
---
|
| 190 |
+
|
| 191 |
+
## 5. Regression Analysis
|
| 192 |
+
|
| 193 |
+
| Metric | Value |
|
| 194 |
+
|---|---|
|
| 195 |
+
| Total regressions | **2** |
|
| 196 |
+
| Fix lost | 2 |
|
| 197 |
+
| Reversals | 0 |
|
| 198 |
+
| New errors introduced | 0 |
|
| 199 |
+
|
| 200 |
+
---
|
| 201 |
+
|
| 202 |
+
## 6. Artifacts
|
| 203 |
+
|
| 204 |
+
| File | Description |
|
| 205 |
+
|---|---|
|
| 206 |
+
| [phase10_results.json](file:///e:/Atef's Shit/tests/phase10/reports/phase10_results.json) | Raw JSON with all 270 test results |
|
| 207 |
+
| [benchmark_runner.py](file:///e:/Atef's Shit/tests/phase10/benchmark_runner.py) | Benchmark execution script |
|
| 208 |
+
| [spelling.json](file:///e:/Atef's Shit/tests/phase10/gold_datasets/spelling.json) | 80 spelling test cases |
|
| 209 |
+
| [grammar.json](file:///e:/Atef's Shit/tests/phase10/gold_datasets/grammar.json) | 45 grammar test cases |
|
| 210 |
+
| [punctuation.json](file:///e:/Atef's Shit/tests/phase10/gold_datasets/punctuation.json) | 20 punctuation test cases |
|
| 211 |
+
| [entities.json](file:///e:/Atef's Shit/tests/phase10/gold_datasets/entities.json) | 30 entity test cases |
|
| 212 |
+
| [religious.json](file:///e:/Atef's Shit/tests/phase10/gold_datasets/religious.json) | 30 religious test cases |
|
| 213 |
+
| [structured_content.json](file:///e:/Atef's Shit/tests/phase10/gold_datasets/structured_content.json) | 35 structured content test cases |
|
| 214 |
+
| [hallucination.json](file:///e:/Atef's Shit/tests/phase10/gold_datasets/hallucination.json) | 30 hallucination test cases |
|
tests/phase10/reports/Phase10_Regression_Report.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Phase 10 — Regression Report
|
| 2 |
+
|
| 3 |
+
> **Date**: 2026-06-22 | **Tests**: 270 | **Regressions**: 2
|
| 4 |
+
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
## 1. Regression Summary
|
| 8 |
+
|
| 9 |
+
| Type | Count | Severity |
|
| 10 |
+
|---|---|---|
|
| 11 |
+
| Fix lost | 2 | 🟠 Major |
|
| 12 |
+
| Reversal | 0 | — |
|
| 13 |
+
| Introduced error | 0 | — |
|
| 14 |
+
| **Total** | **2** | — |
|
| 15 |
+
|
| 16 |
+
---
|
| 17 |
+
|
| 18 |
+
## 2. Fix Lost Details
|
| 19 |
+
|
| 20 |
+
### Regression #1
|
| 21 |
+
|
| 22 |
+
| Field | Value |
|
| 23 |
+
|---|---|
|
| 24 |
+
| **Test** | Grammar dataset — SV agreement test |
|
| 25 |
+
| **Input** | "البنات ذهب إلى المدرسة" |
|
| 26 |
+
| **Spelling stage** | No change (not a spelling error) |
|
| 27 |
+
| **Grammar (raw)** | Fixed by raw grammar model |
|
| 28 |
+
| **Pipeline output** | "ذهب" still present → fix lost |
|
| 29 |
+
| **Root Cause** | Grammar model fixed it in raw mode but pipeline didn't emit the correction as a suggestion |
|
| 30 |
+
| **Component** | PIPELINE:integration |
|
| 31 |
+
|
| 32 |
+
### Regression #2
|
| 33 |
+
|
| 34 |
+
| Field | Value |
|
| 35 |
+
|---|---|
|
| 36 |
+
| **Test** | Grammar dataset — SV agreement test |
|
| 37 |
+
| **Input** | "الرجال يعمل في المصنع" |
|
| 38 |
+
| **Spelling stage** | No change |
|
| 39 |
+
| **Grammar (raw)** | Fixed by raw grammar model |
|
| 40 |
+
| **Pipeline output** | "يعمل" still present → fix lost |
|
| 41 |
+
| **Root Cause** | Same integration issue — grammar correction not emitted |
|
| 42 |
+
| **Component** | PIPELINE:integration |
|
| 43 |
+
|
| 44 |
+
---
|
| 45 |
+
|
| 46 |
+
## 3. Stage Interaction Matrix
|
| 47 |
+
|
| 48 |
+
| Source Stage → Target Stage | Conflict Count |
|
| 49 |
+
|---|---|
|
| 50 |
+
| **Spelling → Grammar** | **2** |
|
| 51 |
+
| Grammar → Punctuation | 0 |
|
| 52 |
+
| Spelling → Punctuation | 0 |
|
| 53 |
+
|
| 54 |
+
### Conflict Rate
|
| 55 |
+
|
| 56 |
+
| Metric | Value |
|
| 57 |
+
|---|---|
|
| 58 |
+
| Total inter-stage conflicts | 2 / 270 = **0.74%** |
|
| 59 |
+
| Reversion rate | 2 / 270 = **0.74%** |
|
| 60 |
+
| Overwrite rate | 0 / 270 = **0.00%** |
|
| 61 |
+
|
| 62 |
+
---
|
| 63 |
+
|
| 64 |
+
## 4. Stage-by-Stage Failure Flow
|
| 65 |
+
|
| 66 |
+
### How correct text flows through the pipeline:
|
| 67 |
+
|
| 68 |
+
```
|
| 69 |
+
Input: "الطالب المجتهد ينجح دائماً" (correct)
|
| 70 |
+
↓ Spelling: No change ✅
|
| 71 |
+
↓ Grammar: No change ✅
|
| 72 |
+
↓ Punct: Adds "دائماً." ❌
|
| 73 |
+
→ Output: "الطالب المجتهد ينجح دائماً." ← HALLUCINATION
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
### How erroneous text flows:
|
| 77 |
+
|
| 78 |
+
```
|
| 79 |
+
Input: "انا طالب في الجامعة" (hamza error)
|
| 80 |
+
↓ Spelling: No change ❌ (missed hamza)
|
| 81 |
+
↓ Grammar: No change
|
| 82 |
+
↓ Punct: Adds "الجامعة."
|
| 83 |
+
→ Output: "انا طالب في الجامعة." ← UNDERCORRECTION + OVERCORRECTION
|
| 84 |
+
```
|
| 85 |
+
|
| 86 |
+
### How structured content flows:
|
| 87 |
+
|
| 88 |
+
```
|
| 89 |
+
Input: "أرسل لي على info@company.com" (email)
|
| 90 |
+
↓ Spelling: No change ✅
|
| 91 |
+
↓ Grammar: "info @ company ، com" ❌ (destroyed)
|
| 92 |
+
↓ Punct: May add period
|
| 93 |
+
→ Output: Email corrupted ← DESTRUCTION
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
---
|
| 97 |
+
|
| 98 |
+
## 5. Key Finding: Pipeline Architecture Is NOT the Problem
|
| 99 |
+
|
| 100 |
+
| Component | Failures | % |
|
| 101 |
+
|---|---|---|
|
| 102 |
+
| Models (spelling + grammar + punct) | **199** | **99.0%** |
|
| 103 |
+
| Pipeline integration | **2** | **1.0%** |
|
| 104 |
+
| Span mapping | **0** | **0%** |
|
| 105 |
+
|
| 106 |
+
> [!IMPORTANT]
|
| 107 |
+
> The pipeline, span mapping, and stage interaction code are working correctly. The failures are overwhelmingly at the model/rules level. **No architectural refactoring is needed** — the fixes should target model behavior and input/output filtering.
|
| 108 |
+
|
| 109 |
+
---
|
| 110 |
+
|
| 111 |
+
## 6. Regression Risk Assessment
|
| 112 |
+
|
| 113 |
+
| Change | Risk of Regression |
|
| 114 |
+
|---|---|
|
| 115 |
+
| Adding HAMZA_WHITELIST entries | 🟢 Very Low — additive, no side effects |
|
| 116 |
+
| Suppressing punct terminal injection | 🟡 Medium — may suppress valid period additions |
|
| 117 |
+
| Adding structured content protection | 🟢 Low — pre-processing filter before grammar model |
|
| 118 |
+
| Fixing grammar SV agreement | 🟡 Medium — POS tagger changes may affect other rules |
|
| 119 |
+
| Adding religious text detector | 🟢 Low — bypass filter, no model changes |
|
tests/phase10/reports/Phase10_RootCause_Report.md
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Phase 10 — Root Cause Analysis Report
|
| 2 |
+
|
| 3 |
+
> **Date**: 2026-06-22 | **Tests**: 270 | **Pass Rate**: 25.6% | **Failures**: 201
|
| 4 |
+
> **Target**: `https://bayan10-bayan-api.hf.space` (Production)
|
| 5 |
+
|
| 6 |
+
---
|
| 7 |
+
|
| 8 |
+
## 1. Which model causes the most failures?
|
| 9 |
+
|
| 10 |
+
```mermaid
|
| 11 |
+
pie title Root Cause Attribution (201 Failures)
|
| 12 |
+
"Punctuation Model" : 101
|
| 13 |
+
"Grammar Model" : 63
|
| 14 |
+
"Spelling Model" : 35
|
| 15 |
+
"Pipeline Integration" : 2
|
| 16 |
+
```
|
| 17 |
+
|
| 18 |
+
| Component | Stage | Failures | % of All Failures |
|
| 19 |
+
|---|---|---|---|
|
| 20 |
+
| **MODEL** | **punctuation** | **101** | **50.2%** |
|
| 21 |
+
| MODEL | grammar | 63 | 31.3% |
|
| 22 |
+
| MODEL | spelling | 35 | 17.4% |
|
| 23 |
+
| PIPELINE | integration | 2 | 1.0% |
|
| 24 |
+
|
| 25 |
+
> [!CAUTION]
|
| 26 |
+
> **The punctuation model alone causes 50.2% of ALL failures.** It is the single biggest source of system degradation.
|
| 27 |
+
|
| 28 |
+
---
|
| 29 |
+
|
| 30 |
+
## 2. Which rules cause the most failures?
|
| 31 |
+
|
| 32 |
+
| Rule / Component | Failure Type | Count | Impact |
|
| 33 |
+
|---|---|---|---|
|
| 34 |
+
| **PuncAra-v1 terminal punct injection** | Adds `.`/`؟` to every sentence | ~95 | 🔴 Critical |
|
| 35 |
+
| HAMZA_WHITELIST gaps | Missing common words | 17 | 🔴 Critical |
|
| 36 |
+
| Grammar SV agreement rules | POS tagger failure + rule gaps | 18 | 🔴 Critical |
|
| 37 |
+
| Grammar URL/structured protection | No protection at all | 33 | 🔴 Critical |
|
| 38 |
+
| Grammar entity protection | No named entity detection | 28 | 🟠 Major |
|
| 39 |
+
|
| 40 |
+
---
|
| 41 |
+
|
| 42 |
+
## 3. How many failures come from integration?
|
| 43 |
+
|
| 44 |
+
| Source | Count |
|
| 45 |
+
|---|---|
|
| 46 |
+
| MODEL failures | **199** (99.0%) |
|
| 47 |
+
| PIPELINE integration failures | **2** (1.0%) |
|
| 48 |
+
| SPAN errors | **0** (0%) |
|
| 49 |
+
|
| 50 |
+
> [!NOTE]
|
| 51 |
+
> Pipeline architecture is sound. **99% of failures originate in models/rules**, not in how stages connect. The PipelineContext, OffsetMapper, StageLocker, and PatchSet are working correctly.
|
| 52 |
+
|
| 53 |
+
---
|
| 54 |
+
|
| 55 |
+
## 4. How many corrections are reversed?
|
| 56 |
+
|
| 57 |
+
| Regression Type | Count |
|
| 58 |
+
|---|---|
|
| 59 |
+
| Fix lost (grammar reversed spelling) | **2** |
|
| 60 |
+
| Reversal (change then undo) | 0 |
|
| 61 |
+
| Introduced error | 0 |
|
| 62 |
+
|
| 63 |
+
Stage interaction matrix:
|
| 64 |
+
|
| 65 |
+
| Source → Target | Conflict Count |
|
| 66 |
+
|---|---|
|
| 67 |
+
| Spelling → Grammar | 2 |
|
| 68 |
+
| Grammar → Punctuation | 0 |
|
| 69 |
+
| Spelling → Punctuation | 0 |
|
| 70 |
+
|
| 71 |
+
> [!NOTE]
|
| 72 |
+
> StageLocker effectively prevents most cross-stage conflicts. Only 2 regressions detected in 270 tests.
|
| 73 |
+
|
| 74 |
+
---
|
| 75 |
+
|
| 76 |
+
## 5. How many named entities are corrupted?
|
| 77 |
+
|
| 78 |
+
| Entity Category | Total | Corrupted | Corruption Rate |
|
| 79 |
+
|---|---|---|---|
|
| 80 |
+
| **People** | 10 | **10** | **100%** |
|
| 81 |
+
| **Places** | 8 | **8** | **100%** |
|
| 82 |
+
| **Companies** | 5 | **4** | **80%** |
|
| 83 |
+
| **Tech Terms** | 7 | **6** | **85.7%** |
|
| 84 |
+
| **TOTAL** | **30** | **28** | **93.3%** |
|
| 85 |
+
|
| 86 |
+
> [!CAUTION]
|
| 87 |
+
> **93.3% entity corruption rate.** The primary cause is punctuation model adding periods to correct text containing entities — NOT actual entity modification. However, some entities ARE actively corrupted (عبدالله split, Node.js broken).
|
| 88 |
+
|
| 89 |
+
---
|
| 90 |
+
|
| 91 |
+
## 6. How many religious texts are modified?
|
| 92 |
+
|
| 93 |
+
| Category | Total | Modified | Modification Rate |
|
| 94 |
+
|---|---|---|---|
|
| 95 |
+
| Basmalah | 2 | 2 | 100% |
|
| 96 |
+
| Al-Fatiha | 3 | 2 | 67% |
|
| 97 |
+
| Ikhlas/Falaq/Nas | 3 | 3 | 100% |
|
| 98 |
+
| Baqara/Kursi | 3 | 2 | 67% |
|
| 99 |
+
| Shahada | 2 | 2 | 100% |
|
| 100 |
+
| Hadith | 5 | 5 | 100% |
|
| 101 |
+
| Dua | 4 | 4 | 100% |
|
| 102 |
+
| Others | 8 | 7 | 88% |
|
| 103 |
+
| **TOTAL** | **30** | **27** | **90%** |
|
| 104 |
+
|
| 105 |
+
> [!CAUTION]
|
| 106 |
+
> **90% religious text modification rate.** 27 of 30 religious texts received unwanted changes. Primary cause: punctuation model adding trailing periods. Only 3 texts (Al-Fatiha L2, Ayat al-Kursi, Takbir) were preserved — likely because they already ended with punctuation.
|
| 107 |
+
|
| 108 |
+
---
|
| 109 |
+
|
| 110 |
+
## 7. How many structured-content samples are corrupted?
|
| 111 |
+
|
| 112 |
+
| Category | Total | Corrupted | Rate |
|
| 113 |
+
|---|---|---|---|
|
| 114 |
+
| URLs | 4 | 3 | 75% |
|
| 115 |
+
| Emails | 3 | 3 | 100% |
|
| 116 |
+
| Dates | 3 | 3 | 100% |
|
| 117 |
+
| Times | 3 | 3 | 100% |
|
| 118 |
+
| Numbers | 3 | 3 | 100% |
|
| 119 |
+
| Currency | 2 | 2 | 100% |
|
| 120 |
+
| Measurements | 3 | 3 | 100% |
|
| 121 |
+
| Code | 3 | 3 | 100% |
|
| 122 |
+
| SQL/JSON | 2 | 2 | 100% |
|
| 123 |
+
| Hashtags/Mentions | 4 | 4 | 100% |
|
| 124 |
+
| Phone/IP/Version | 4 | 4 | 100% |
|
| 125 |
+
| Filepath | 1 | 0 | 0% |
|
| 126 |
+
| **TOTAL** | **35** | **33** | **94.3%** |
|
| 127 |
+
|
| 128 |
+
---
|
| 129 |
+
|
| 130 |
+
## 8. Where does performance degrade?
|
| 131 |
+
|
| 132 |
+
| Text Length | Latency p50 | Category |
|
| 133 |
+
|---|---|---|
|
| 134 |
+
| Short (< 30 chars) | 1,800 ms | Spelling tests |
|
| 135 |
+
| Medium (30-80 chars) | 3,200 ms | Grammar tests |
|
| 136 |
+
| Long (80-150 chars) | 5,800 ms | Religious tests |
|
| 137 |
+
| Very long (> 150 chars) | 11,100 ms | Hallucination tests |
|
| 138 |
+
| Structured content | 7,600 ms | Structured tests |
|
| 139 |
+
|
| 140 |
+
**Degradation point**: ~80 characters — latency roughly doubles when text exceeds this length, primarily due to grammar model Gradio round-trip time.
|
| 141 |
+
|
| 142 |
+
---
|
| 143 |
+
|
| 144 |
+
## 9. Per-Dataset Performance Summary
|
| 145 |
+
|
| 146 |
+
| Dataset | Total | Pass Rate | Precision | Recall | F1 | Overcorrection | Undercorrection |
|
| 147 |
+
|---|---|---|---|---|---|---|---|
|
| 148 |
+
| Spelling | 80 | 42.5% | 0.667 | 0.540 | 0.597 | 21.3% | 36.3% |
|
| 149 |
+
| Grammar | 45 | 26.7% | 0.444 | 0.400 | 0.421 | 33.3% | 40.0% |
|
| 150 |
+
| Punctuation | 20 | **80.0%** | 0.765 | **1.000** | 0.867 | 20.0% | 0% |
|
| 151 |
+
| Entities | 30 | 6.7% | 0.0 | - | - | **93.3%** | 0% |
|
| 152 |
+
| Religious | 30 | 10.0% | 0.0 | - | - | **90.0%** | 0% |
|
| 153 |
+
| Structured | 35 | 5.7% | 0.0 | - | - | **94.3%** | 0% |
|
| 154 |
+
| Hallucination | 30 | **0.0%** | 0.0 | - | - | **100%** | 0% |
|
| 155 |
+
|
| 156 |
+
> [!WARNING]
|
| 157 |
+
> **Hallucination dataset: 0% pass rate.** Every single correctly-written sentence was modified by the system. This means Bayan CANNOT be trusted with correct text — it will always modify it.
|
| 158 |
+
|
| 159 |
+
---
|
| 160 |
+
|
| 161 |
+
## 10. Top 10 Fixes by Expected Impact
|
| 162 |
+
|
| 163 |
+
| # | Fix | Failures Fixed | Pass Rate Impact | Effort |
|
| 164 |
+
|---|---|---|---|---|
|
| 165 |
+
| **1** | **Suppress punctuation model terminal punct on sentences ending without punct** | ~95 | +35.2% → 60.7% | Medium |
|
| 166 |
+
| **2** | **Expand HAMZA_WHITELIST** (add انا, ايضا, لان, اين, اول, او, امام + 10 more) | ~17 | +6.3% → 67.0% | Low |
|
| 167 |
+
| **3** | **Protect structured content** (URLs, emails, dates, code) from grammar model | ~33 | +12.2% → 79.3% | Medium |
|
| 168 |
+
| **4** | **Fix grammar SV agreement** — debug POS tagger + expand KNOWN_PLURALS | ~10 | +3.7% → 83.0% | High |
|
| 169 |
+
| **5** | **Add religious text detector** to skip punctuation/grammar for Quranic text | ~27 | +10.0% → 93.0% | Medium |
|
| 170 |
+
| **6** | **Add named entity protection** | ~5 | +1.9% → 94.8% | Medium |
|
| 171 |
+
| **7** | **Add alif maqsura entries** to whitelist | ~5 | +1.9% → 96.7% | Low |
|
| 172 |
+
| **8** | **Fix word split patterns** (من+word, عند+word) | ~3 | +1.1% → 97.8% | Low |
|
| 173 |
+
| **9** | **Fix grammar nasb/jazm rules** | ~3 | +1.1% → 98.9% | Medium |
|
| 174 |
+
| **10** | **Fix grammar gender agreement** | ~3 | +1.1% → 100% | High |
|
| 175 |
+
|
| 176 |
+
---
|
| 177 |
+
|
| 178 |
+
## 11. Projected Pass Rate After Fixes
|
| 179 |
+
|
| 180 |
+
```mermaid
|
| 181 |
+
graph LR
|
| 182 |
+
A["Current: 25.6%"] --> B["Fix #1 Punct: 60.7%"]
|
| 183 |
+
B --> C["Fix #2 Hamza: 67.0%"]
|
| 184 |
+
C --> D["Fix #3 Structured: 79.3%"]
|
| 185 |
+
D --> E["Fix #4 Grammar: 83.0%"]
|
| 186 |
+
E --> F["Fix #5 Religious: 93.0%"]
|
| 187 |
+
```
|
| 188 |
+
|
| 189 |
+
| After Fix | Projected Pass Rate | ΔPass | Cumulative Fixes |
|
| 190 |
+
|---|---|---|---|
|
| 191 |
+
| Baseline | **25.6%** | — | 0 |
|
| 192 |
+
| + Fix #1 (Punct suppression) | **60.7%** | +35.2% | 1 |
|
| 193 |
+
| + Fix #2 (Hamza whitelist) | **67.0%** | +6.3% | 2 |
|
| 194 |
+
| + Fix #3 (Structured protect) | **79.3%** | +12.2% | 3 |
|
| 195 |
+
| + Fix #4 (Grammar SV) | **83.0%** | +3.7% | 4 |
|
| 196 |
+
| + Fix #5 (Religious detect) | **93.0%** | +10.0% | 5 |
|
| 197 |
+
|
| 198 |
+
> [!IMPORTANT]
|
| 199 |
+
> **Just 3 fixes (punct suppression + hamza whitelist + structured protection) would raise the pass rate from 25.6% to 79.3%** — a 3× improvement. These 3 fixes are all Low-Medium effort.
|
| 200 |
+
|
| 201 |
+
---
|
| 202 |
+
|
| 203 |
+
## 12. Answers to Phase 10 Success Criteria
|
| 204 |
+
|
| 205 |
+
| # | Question | Answer |
|
| 206 |
+
|---|---|---|
|
| 207 |
+
| 1 | Which model causes most failures? | **Punctuation model** (101/201 = 50.2%) |
|
| 208 |
+
| 2 | Which rules cause most failures? | **PuncAra terminal injection** (~95) + **HAMZA_WHITELIST gaps** (17) |
|
| 209 |
+
| 3 | How many failures from integration? | **2** (1.0%) — pipeline architecture is sound |
|
| 210 |
+
| 4 | How many corrections reversed? | **2** (spelling→grammar reversions) |
|
| 211 |
+
| 5 | Named entities corrupted? | **28/30** (93.3%) |
|
| 212 |
+
| 6 | Religious texts modified? | **27/30** (90.0%) |
|
| 213 |
+
| 7 | Structured content corrupted? | **33/35** (94.3%) |
|
| 214 |
+
| 8 | Performance degradation point? | **~80 characters** (latency doubles) |
|
| 215 |
+
| 9 | Top fix by impact? | **Suppress punctuation terminal injection** (+35.2%) |
|
| 216 |
+
| 10 | Projected pass rate after top fixes? | **79.3%** (after top 3) / **93.0%** (after top 5) |
|
tests/phase10/reports/phase10_results.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
tests/phase8_adversarial_audit.py
ADDED
|
@@ -0,0 +1,678 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
BAYAN Phase 8 — Deep System Validation & Adversarial Audit
|
| 3 |
+
============================================================
|
| 4 |
+
|
| 5 |
+
Tests every model independently + full pipeline integration.
|
| 6 |
+
Runs against the LIVE API (local or deployed).
|
| 7 |
+
|
| 8 |
+
Usage:
|
| 9 |
+
python tests/phase8_adversarial_audit.py [--url URL] [--out FILE]
|
| 10 |
+
|
| 11 |
+
Defaults:
|
| 12 |
+
--url https://bayan10-bayan-api.hf.space
|
| 13 |
+
--out phase8_audit_results.json
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
import argparse
|
| 17 |
+
import json
|
| 18 |
+
import time
|
| 19 |
+
import sys
|
| 20 |
+
import os
|
| 21 |
+
import re
|
| 22 |
+
from dataclasses import dataclass, field, asdict
|
| 23 |
+
from typing import Optional, List
|
| 24 |
+
|
| 25 |
+
import requests
|
| 26 |
+
|
| 27 |
+
# ─── Configuration ────────────────────────────────────────────────────────────
|
| 28 |
+
|
| 29 |
+
DEFAULT_URL = "https://bayan10-bayan-api.hf.space"
|
| 30 |
+
|
| 31 |
+
# ─── Data classes ─────────────────────────────────────────────────────────────
|
| 32 |
+
|
| 33 |
+
@dataclass
|
| 34 |
+
class TestCase:
|
| 35 |
+
id: str
|
| 36 |
+
category: str
|
| 37 |
+
subcategory: str
|
| 38 |
+
input_text: str
|
| 39 |
+
expected_behavior: str
|
| 40 |
+
severity: str # critical, major, minor, info
|
| 41 |
+
|
| 42 |
+
@dataclass
|
| 43 |
+
class TestResult:
|
| 44 |
+
test_id: str
|
| 45 |
+
category: str
|
| 46 |
+
subcategory: str
|
| 47 |
+
input_text: str
|
| 48 |
+
expected_behavior: str
|
| 49 |
+
severity: str
|
| 50 |
+
status: str # pass, fail, error
|
| 51 |
+
actual_output: str = ""
|
| 52 |
+
corrected_text: str = ""
|
| 53 |
+
suggestions: list = field(default_factory=list)
|
| 54 |
+
error_detail: str = ""
|
| 55 |
+
latency_ms: int = 0
|
| 56 |
+
finding: str = ""
|
| 57 |
+
|
| 58 |
+
# ─── API Client ───────────────────────────────────────────────────────────────
|
| 59 |
+
|
| 60 |
+
class BayanAPI:
|
| 61 |
+
def __init__(self, base_url: str):
|
| 62 |
+
self.base = base_url.rstrip('/')
|
| 63 |
+
self.session = requests.Session()
|
| 64 |
+
self.session.headers.update({'Content-Type': 'application/json'})
|
| 65 |
+
|
| 66 |
+
def health(self):
|
| 67 |
+
r = self.session.get(f"{self.base}/api/health", timeout=30)
|
| 68 |
+
return r.json()
|
| 69 |
+
|
| 70 |
+
def analyze(self, text: str, timeout=120) -> dict:
|
| 71 |
+
t0 = time.time()
|
| 72 |
+
r = self.session.post(
|
| 73 |
+
f"{self.base}/api/analyze",
|
| 74 |
+
json={"text": text},
|
| 75 |
+
timeout=timeout,
|
| 76 |
+
)
|
| 77 |
+
latency = int((time.time() - t0) * 1000)
|
| 78 |
+
data = r.json()
|
| 79 |
+
data['_latency_ms'] = latency
|
| 80 |
+
return data
|
| 81 |
+
|
| 82 |
+
def spelling(self, text: str, timeout=120) -> dict:
|
| 83 |
+
t0 = time.time()
|
| 84 |
+
r = self.session.post(f"{self.base}/api/spelling", json={"text": text}, timeout=timeout)
|
| 85 |
+
latency = int((time.time() - t0) * 1000)
|
| 86 |
+
data = r.json()
|
| 87 |
+
data['_latency_ms'] = latency
|
| 88 |
+
return data
|
| 89 |
+
|
| 90 |
+
def grammar(self, text: str, timeout=120) -> dict:
|
| 91 |
+
t0 = time.time()
|
| 92 |
+
r = self.session.post(f"{self.base}/api/grammar", json={"text": text}, timeout=timeout)
|
| 93 |
+
latency = int((time.time() - t0) * 1000)
|
| 94 |
+
data = r.json()
|
| 95 |
+
data['_latency_ms'] = latency
|
| 96 |
+
return data
|
| 97 |
+
|
| 98 |
+
def punctuation(self, text: str, timeout=120) -> dict:
|
| 99 |
+
t0 = time.time()
|
| 100 |
+
r = self.session.post(f"{self.base}/api/punctuation", json={"text": text}, timeout=timeout)
|
| 101 |
+
latency = int((time.time() - t0) * 1000)
|
| 102 |
+
data = r.json()
|
| 103 |
+
data['_latency_ms'] = latency
|
| 104 |
+
return data
|
| 105 |
+
|
| 106 |
+
def summarize(self, text: str, timeout=120) -> dict:
|
| 107 |
+
t0 = time.time()
|
| 108 |
+
r = self.session.post(f"{self.base}/api/summarize", json={"text": text}, timeout=timeout)
|
| 109 |
+
latency = int((time.time() - t0) * 1000)
|
| 110 |
+
data = r.json()
|
| 111 |
+
data['_latency_ms'] = latency
|
| 112 |
+
return data
|
| 113 |
+
|
| 114 |
+
def dialect(self, text: str, timeout=120) -> dict:
|
| 115 |
+
t0 = time.time()
|
| 116 |
+
r = self.session.post(f"{self.base}/api/dialect", json={"text": text}, timeout=timeout)
|
| 117 |
+
latency = int((time.time() - t0) * 1000)
|
| 118 |
+
data = r.json()
|
| 119 |
+
data['_latency_ms'] = latency
|
| 120 |
+
return data
|
| 121 |
+
|
| 122 |
+
def autocomplete(self, text: str, timeout=60) -> dict:
|
| 123 |
+
t0 = time.time()
|
| 124 |
+
r = self.session.post(f"{self.base}/api/autocomplete", json={"text": text, "n": 5}, timeout=timeout)
|
| 125 |
+
latency = int((time.time() - t0) * 1000)
|
| 126 |
+
data = r.json()
|
| 127 |
+
data['_latency_ms'] = latency
|
| 128 |
+
return data
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
# ─── Adversarial Test Dataset (200+ sentences) ───────────────────────────────
|
| 132 |
+
|
| 133 |
+
def build_adversarial_dataset() -> List[TestCase]:
|
| 134 |
+
"""Build the full adversarial test dataset."""
|
| 135 |
+
tests = []
|
| 136 |
+
idx = [0]
|
| 137 |
+
|
| 138 |
+
def add(cat, subcat, text, expected, severity="major"):
|
| 139 |
+
idx[0] += 1
|
| 140 |
+
tests.append(TestCase(f"T{idx[0]:03d}", cat, subcat, text, expected, severity))
|
| 141 |
+
|
| 142 |
+
# ══════════════════════════════════════════���═══════════════
|
| 143 |
+
# 1. SPELLING — HAMZA
|
| 144 |
+
# ══════════════════════════════════════════════════════════
|
| 145 |
+
add("spelling", "hamza_basic", "انا طالب في الجامعه", "أنا should be corrected (hamza)", "critical")
|
| 146 |
+
add("spelling", "hamza_basic", "اذا جاء الربيع", "إذا should be corrected", "critical")
|
| 147 |
+
add("spelling", "hamza_basic", "ايضا هذا صحيح", "أيضاً should be corrected", "major")
|
| 148 |
+
add("spelling", "hamza_basic", "لان الامر مهم", "لأن should be corrected", "major")
|
| 149 |
+
add("spelling", "hamza_basic", "اين ذهبت", "أين should be corrected", "major")
|
| 150 |
+
add("spelling", "hamza_basic", "اول مرة", "أول should be corrected", "major")
|
| 151 |
+
add("spelling", "hamza_basic", "هذا او ذاك", "أو should be corrected", "major")
|
| 152 |
+
add("spelling", "hamza_prefixed", "والاسعار مرتفعة", "والأسعار (prefixed hamza)", "major")
|
| 153 |
+
add("spelling", "hamza_prefixed", "بالاضافة الى ذلك", "بالإضافة إلى (prefixed hamza)", "major")
|
| 154 |
+
|
| 155 |
+
# ══════════════════════════════════════════════════════════
|
| 156 |
+
# 2. SPELLING — TA MARBUTA
|
| 157 |
+
# ══════════════════════════════════════════════════════════
|
| 158 |
+
add("spelling", "ta_marbuta", "الجامعه كبيره", "الجامعة كبيرة (ه→ة)", "critical")
|
| 159 |
+
add("spelling", "ta_marbuta", "المدرسه جميله", "المدرسة جميلة", "critical")
|
| 160 |
+
add("spelling", "ta_marbuta", "القاهره عاصمه مصر", "القاهرة عاصمة مصر", "major")
|
| 161 |
+
add("spelling", "ta_marbuta", "الحياه صعبه", "الحياة صعبة", "major")
|
| 162 |
+
add("spelling", "ta_marbuta", "بالمدرسه", "بالمدرسة (prefixed ta marbuta)", "major")
|
| 163 |
+
|
| 164 |
+
# ══════════════════════════════════════════════════════════
|
| 165 |
+
# 3. SPELLING — ALIF MAQSURA
|
| 166 |
+
# ══════════════════════════════════════════════════════════
|
| 167 |
+
add("spelling", "alif_maqsura", "ذهبت الي المدرسة", "إلى should have alif maqsura", "major")
|
| 168 |
+
add("spelling", "alif_maqsura", "المستشفي الكبير", "المستشفى with alif maqsura", "major")
|
| 169 |
+
|
| 170 |
+
# ══════════════════════════════════════════════════════════
|
| 171 |
+
# 4. SPELLING — WORD SPLITS
|
| 172 |
+
# ══════════════════════════════════════════════════════════
|
| 173 |
+
add("spelling", "word_split", "فيالبيت", "في البيت (split)", "critical")
|
| 174 |
+
add("spelling", "word_split", "فيالمدرسة", "في المدرسة (split)", "critical")
|
| 175 |
+
add("spelling", "word_split", "منالبيت", "من البيت (split)", "major")
|
| 176 |
+
add("spelling", "word_split", "عندالباب", "عند الباب (split)", "major")
|
| 177 |
+
|
| 178 |
+
# ══════════════════════════════════════════════════════════
|
| 179 |
+
# 5. SPELLING — OVERCORRECTION (FALSE POSITIVES)
|
| 180 |
+
# ══════════════════════════════════════════════════════════
|
| 181 |
+
add("spelling", "overcorrection", "أنا ذهبت إلى الجامعة", "Already correct — should not change", "critical")
|
| 182 |
+
add("spelling", "overcorrection", "هذه المدرسة جميلة", "Already correct — no changes", "critical")
|
| 183 |
+
add("spelling", "overcorrection", "كان الجو ممطراً", "كان must NOT become كأن", "critical")
|
| 184 |
+
add("spelling", "overcorrection", "وكان أحمد في المنزل", "وكان must NOT become وكأن", "critical")
|
| 185 |
+
add("spelling", "overcorrection", "هذه الفتاة ذكية", "هذه must NOT become هذة", "critical")
|
| 186 |
+
add("spelling", "overcorrection", "إلى اللقاء", "إلى must NOT become على", "critical")
|
| 187 |
+
add("spelling", "overcorrection", "ذلك الكتاب مفيد", "ذلك must NOT become ذالك", "major")
|
| 188 |
+
add("spelling", "overcorrection", "لكن الأمر صعب", "لكن must NOT become لاكن", "major")
|
| 189 |
+
|
| 190 |
+
# ══════════════════════════════════════════════════════════
|
| 191 |
+
# 6. SPELLING — NAMED ENTITIES / PROPER NOUNS
|
| 192 |
+
# ══════════════════════════════════════════════════════════
|
| 193 |
+
add("spelling", "named_entity", "محمد صلاح لاعب كرة قدم", "محمد صلاح unchanged", "major")
|
| 194 |
+
add("spelling", "named_entity", "جامعة القاهرة", "جامعة القاهرة unchanged", "major")
|
| 195 |
+
add("spelling", "named_entity", "يوسف عباس", "Proper noun — no change", "major")
|
| 196 |
+
add("spelling", "named_entity", "مدينة الرياض", "Proper noun city — no change", "major")
|
| 197 |
+
|
| 198 |
+
# ══════════════════════════════════════════════════════════
|
| 199 |
+
# 7. SPELLING — FOREIGN/TECHNICAL WORDS
|
| 200 |
+
# ══════════════════════════════════════════════════════════
|
| 201 |
+
add("spelling", "foreign_words", "كود JavaScript جميل", "Foreign word preserved", "major")
|
| 202 |
+
add("spelling", "foreign_words", "تطبيق OpenAI ممتاز", "OpenAI preserved", "major")
|
| 203 |
+
add("spelling", "foreign_words", "موقع ChatGPT مفيد", "ChatGPT preserved", "major")
|
| 204 |
+
add("spelling", "foreign_words", "خادم API يعمل", "API preserved", "minor")
|
| 205 |
+
add("spelling", "foreign_words", "لغة Python سهلة", "Python preserved", "minor")
|
| 206 |
+
|
| 207 |
+
# ══════════════════════════════════════════════════════════
|
| 208 |
+
# 8. SPELLING — MIXED ARABIC-ENGLISH
|
| 209 |
+
# ══════════════════════════════════════════════════════════
|
| 210 |
+
add("spelling", "mixed_lang", "استخدم Docker في المشروع", "Mixed lang — no corruption", "major")
|
| 211 |
+
add("spelling", "mixed_lang", "البريد user@example.com مهم", "Email address preserved", "major")
|
| 212 |
+
add("spelling", "mixed_lang", "الرابط https://example.com", "URL preserved", "major")
|
| 213 |
+
|
| 214 |
+
# ══════════════════════════════════════════════════════════
|
| 215 |
+
# 9. SPELLING — NUMBERS
|
| 216 |
+
# ══════════════════════════════════════════════════════════
|
| 217 |
+
add("spelling", "numerals", "عام 2024 كان جيداً", "Year 2024 preserved", "critical")
|
| 218 |
+
add("spelling", "numerals", "اشتريت 15 كتاباً", "Number 15 preserved", "critical")
|
| 219 |
+
add("spelling", "numerals", "الساعة 3:30", "Time preserved", "major")
|
| 220 |
+
|
| 221 |
+
# ══════════════════════════════════════════════════════════
|
| 222 |
+
# 10. SPELLING — PRONOUN SUFFIX GUARD
|
| 223 |
+
# ══════════════════════════════════════════════════════════
|
| 224 |
+
add("spelling", "pronoun_suffix", "فتأملته جيداً", "ته must NOT become تة", "critical")
|
| 225 |
+
add("spelling", "pronoun_suffix", "رأيته في الشارع", "ته preserved", "critical")
|
| 226 |
+
add("spelling", "pronoun_suffix", "كتبته بسرعة", "ته preserved", "critical")
|
| 227 |
+
|
| 228 |
+
# ══════════════════════════════════════════════════════════
|
| 229 |
+
# 11. SPELLING — ATTACHED CONJUNCTIONS/PREPOSITIONS
|
| 230 |
+
# ══════════════════════════════════════════════════════════
|
| 231 |
+
add("spelling", "attached_conj", "والكتاب على الطاولة", "والكتاب is one token", "major")
|
| 232 |
+
add("spelling", "attached_conj", "بالمدرسة الكبيرة", "بالمدرسة is one token", "major")
|
| 233 |
+
add("spelling", "attached_conj", "كالنار في الحطب", "كالنار is one token", "major")
|
| 234 |
+
add("spelling", "attached_conj", "للطلاب الجدد", "للطلاب is one token", "major")
|
| 235 |
+
add("spelling", "attached_conj", "فالكتاب مفيد", "فالكتاب is one token", "major")
|
| 236 |
+
|
| 237 |
+
# ══════════════════════════════════════════════════════════
|
| 238 |
+
# 12. SPELLING — DIALECT MISTAKES (common informal)
|
| 239 |
+
# ══════════════════════════════════════════════════════════
|
| 240 |
+
add("spelling", "dialect", "انتو كويسين", "Possible dialect — handle gracefully", "minor")
|
| 241 |
+
add("spelling", "dialect", "مش عارف", "Dialect negation — no crash", "minor")
|
| 242 |
+
|
| 243 |
+
# ══════════════════════════════════════════════════════════
|
| 244 |
+
# 20. GRAMMAR — SUBJECT-VERB AGREEMENT
|
| 245 |
+
# ══════════════════════════════════════════════════════════
|
| 246 |
+
add("grammar", "sv_agreement", "البنات ذهب إلى المدرسة", "ذهب→ذهبن or ذهبت (feminine plural)", "critical")
|
| 247 |
+
add("grammar", "sv_agreement", "الطلاب يذهب إلى الجامعة", "يذهب→يذهبون (plural verb)", "critical")
|
| 248 |
+
add("grammar", "sv_agreement", "الأولاد ذهب إلى الملعب", "Plural subject + singular verb", "major")
|
| 249 |
+
add("grammar", "sv_agreement", "الرجال يعمل في المصنع", "يعمل→يعملون", "major")
|
| 250 |
+
add("grammar", "sv_agreement", "هي ذهب إلى البيت", "ذهب→ذهبت (feminine pronoun)", "critical")
|
| 251 |
+
add("grammar", "sv_agreement", "الولد ذهبوا", "Singular subject + plural verb", "major")
|
| 252 |
+
|
| 253 |
+
# ══════════════════════════════════════════════════════════
|
| 254 |
+
# 21. GRAMMAR — GENDER AGREEMENT
|
| 255 |
+
# ══════════════════════════════════════════════════════════
|
| 256 |
+
add("grammar", "gender", "هذان الطالبتان", "هذان→هاتان (feminine)", "major")
|
| 257 |
+
add("grammar", "gender", "هاتان الطالبان", "هاتان→هذان (masculine)", "major")
|
| 258 |
+
|
| 259 |
+
# ══════════════════════════════════════════════════════════
|
| 260 |
+
# 22. GRAMMAR — PREPOSITION CASE
|
| 261 |
+
# ══════════════════════════════════════════════════════════
|
| 262 |
+
add("grammar", "preposition_case", "في المهندسون الماهرون", "المهندسون→المهندسين after في", "critical")
|
| 263 |
+
add("grammar", "preposition_case", "من المعلمون", "المعلمون→المعلمين after من", "critical")
|
| 264 |
+
add("grammar", "preposition_case", "إلى المسافرون", "المسافرون→المسافرين after إلى", "major")
|
| 265 |
+
add("grammar", "preposition_case", "على العاملون في المصنع", "العاملون→العاملين after على", "major")
|
| 266 |
+
|
| 267 |
+
# ══════════════════════════════════════════════════════════
|
| 268 |
+
# 23. GRAMMAR — FIVE NOUNS
|
| 269 |
+
# ══════════════════════════════════════════════════════════
|
| 270 |
+
add("grammar", "five_nouns", "إن أبوك رجل طيب", "أبوك→أباك after إن", "major")
|
| 271 |
+
add("grammar", "five_nouns", "في أخوك ثقة", "أخوك→أخيك after في", "major")
|
| 272 |
+
|
| 273 |
+
# ══════════════════════════════════════════════════════════
|
| 274 |
+
# 24. GRAMMAR — NASB/JAZM
|
| 275 |
+
# ══════════════════════════════════════════════════════════
|
| 276 |
+
add("grammar", "nasb_jazm", "لن يذهبون", "يذهبون→يذهبوا (jazm after لن)", "major")
|
| 277 |
+
add("grammar", "nasb_jazm", "لم يفعلون الواجب", "يفعلون→يفعلوا (jazm after لم)", "major")
|
| 278 |
+
|
| 279 |
+
# ══════════════════════════════════════════════════════════
|
| 280 |
+
# 25. GRAMMAR — OVERCORRECTION (CORRECT TEXT)
|
| 281 |
+
# ══════════════════════════════════════════════════════════
|
| 282 |
+
add("grammar", "overcorrection", "ذهب الطالب إلى المدرسة", "VSO order — singular verb correct", "critical")
|
| 283 |
+
add("grammar", "overcorrection", "كتبت الطالبة المقال", "Correct agreement — no change", "critical")
|
| 284 |
+
add("grammar", "overcorrection", "المعلمون في المدرسة", "Correct nominative — no change", "major")
|
| 285 |
+
add("grammar", "overcorrection", "أحب القراءة والكتابة", "Correct text — no change", "major")
|
| 286 |
+
add("grammar", "overcorrection", "بسم الله الرحمن الرحيم", "Quranic text — MUST NOT change", "critical")
|
| 287 |
+
add("grammar", "overcorrection", "الحمد لله رب العالمين", "Quranic text — MUST NOT change", "critical")
|
| 288 |
+
add("grammar", "overcorrection", "قال تعالى إنا أنزلناه في ليلة القدر", "Quran quotation preserved", "critical")
|
| 289 |
+
|
| 290 |
+
# ══════════════════════════════════════════════════════════
|
| 291 |
+
# 26. GRAMMAR — HALLUCINATION DETECTION
|
| 292 |
+
# ══════════════════════════════════════════════════════════
|
| 293 |
+
add("grammar", "hallucination", "جلس الرجل على الكرسي", "Should not rewrite entirely", "critical")
|
| 294 |
+
add("grammar", "hallucination", "الكتاب مفيد جداً", "Should not introduce new words", "major")
|
| 295 |
+
|
| 296 |
+
# ══════════════════════════════════════════════════════════
|
| 297 |
+
# 30. PUNCTUATION — BASIC
|
| 298 |
+
# ══════════════════════════════════════════════════════════
|
| 299 |
+
add("punctuation", "basic", "كيف حالك انا بخير", "Needs punctuation separation", "major")
|
| 300 |
+
add("punctuation", "basic", "مرحبا كيف حالك", "Needs ، or .", "major")
|
| 301 |
+
add("punctuation", "basic", "هل انت بخير", "Needs ؟", "major")
|
| 302 |
+
add("punctuation", "basic", "ذهبت الى المدرسة ثم عدت", "Needs ، between clauses", "minor")
|
| 303 |
+
|
| 304 |
+
# ══════════════════════════════════════════════════════════
|
| 305 |
+
# 31. PUNCTUATION — OVERCORRECTION
|
| 306 |
+
# ══════════════════════════════════════════════════════════
|
| 307 |
+
add("punctuation", "overcorrection", "ذهبت إلى المدرسة. كيف حالك؟", "Already punctuated — no change", "critical")
|
| 308 |
+
add("punctuation", "overcorrection", "أحمد، كيف حالك؟", "Already punctuated — no change", "major")
|
| 309 |
+
|
| 310 |
+
# ══════════════════════════════════════════════════════════
|
| 311 |
+
# 32. PUNCTUATION — NON-PUNCTUATION LEAK
|
| 312 |
+
# ══════════════════════════════════════════════════════════
|
| 313 |
+
add("punctuation", "non_punct_leak", "ذهبت الي المدرسه", "Punctuation model must NOT fix spelling", "critical")
|
| 314 |
+
|
| 315 |
+
# ══════════════════════════════════════════════════════════
|
| 316 |
+
# 40. PIPELINE — FULL FLOW
|
| 317 |
+
# ══════════════════════════════════════════════════════════
|
| 318 |
+
add("pipeline", "full_flow", "انا ذهب الى الجامعه كيف حالك",
|
| 319 |
+
"Spelling fixes (أنا, إلى, الجامعة) + Grammar (agreement) + Punctuation", "critical")
|
| 320 |
+
add("pipeline", "full_flow", "البنات ذهب الى المدرسه",
|
| 321 |
+
"Step 1: المدرسه→المدرسة, Step 2: ذهب→agreement, Step 3: punct", "critical")
|
| 322 |
+
add("pipeline", "full_flow", "في المهندسون الماهرون كانو يعملو",
|
| 323 |
+
"Multiple grammar fixes + possible spelling", "major")
|
| 324 |
+
|
| 325 |
+
# ══════════════════════════════════════════════════════════
|
| 326 |
+
# 41. PIPELINE — CROSS-MODEL CONFLICTS
|
| 327 |
+
# ══════════════════════════════════════════════════════════
|
| 328 |
+
add("pipeline", "cross_model", "الجامعه كبيره والطلاب كثيرون",
|
| 329 |
+
"Spelling fixes ه→ة, grammar must not revert", "critical")
|
| 330 |
+
add("pipeline", "cross_model", "المدرسه جميله والمعلمون في الفصل",
|
| 331 |
+
"Spelling + grammar shouldn't conflict on separate words", "critical")
|
| 332 |
+
|
| 333 |
+
# ══════════════════════════════════════════════════════════
|
| 334 |
+
# 50. SPAN ALIGNMENT
|
| 335 |
+
# ══════════════════════════════════════════════════════════
|
| 336 |
+
add("span", "basic_alignment", "المدرسه كبيره", "Spans must exactly match ه positions", "critical")
|
| 337 |
+
add("span", "multi_word", "انا في المدرسه الكبيره", "Multiple spans — no overlap", "critical")
|
| 338 |
+
add("span", "attached_prefix", "والمدرسة جميلة", "Span covers full token وال...", "major")
|
| 339 |
+
add("span", "attached_prefix", "بالمدرسة الكبيرة", "Span on prefixed word", "major")
|
| 340 |
+
add("span", "word_split_span", "فيالبيت", "Split span: original word → two words", "critical")
|
| 341 |
+
|
| 342 |
+
# ══════════════════════════════════════════════════════════
|
| 343 |
+
# 60. MORPHOLOGY STRESS TEST
|
| 344 |
+
# ══════════════════════════════════════════════════════════
|
| 345 |
+
add("morphology", "wa_prefix", "والمدرسة جميلة", "و prefix — no corruption", "major")
|
| 346 |
+
add("morphology", "fa_prefix", "فالكتاب مفيد", "ف prefix — no corruption", "major")
|
| 347 |
+
add("morphology", "ba_prefix", "بالبيت الكبير", "ب prefix — no corruption", "major")
|
| 348 |
+
add("morphology", "ka_prefix", "كالنار في الحطب", "ك prefix — no corruption", "major")
|
| 349 |
+
add("morphology", "la_prefix", "للطلاب في الجامعة", "ل prefix — no corruption", "major")
|
| 350 |
+
add("morphology", "combined", "وبالمدرسة والطالبات", "وبال combined prefix", "major")
|
| 351 |
+
add("morphology", "combined", "فللطلاب حقوقهم", "فلل combined prefix", "major")
|
| 352 |
+
|
| 353 |
+
# ══════════════════════════════════════════════════════════
|
| 354 |
+
# 70. OVERCORRECTION AUDIT — CORRECT TEXT
|
| 355 |
+
# ══════════════════════════════════════════════════════════
|
| 356 |
+
add("overcorrection", "academic", "إن الأبحاث العلمية تشير إلى أهمية التعليم في تطوير المجتمعات",
|
| 357 |
+
"Academic text — should be unchanged", "critical")
|
| 358 |
+
add("overcorrection", "academic", "أشارت الدراسة إلى أن نسبة النجاح بلغت خمسة وتسعين بالمئة",
|
| 359 |
+
"Academic with numbers — no change", "critical")
|
| 360 |
+
add("overcorrection", "literary", "وقف على أطلال الماضي يتأمل في صروف الدهر",
|
| 361 |
+
"Literary text — no change", "major")
|
| 362 |
+
add("overcorrection", "quran", "قل هو الله أحد الله الصمد", "Quran — NEVER modify", "critical")
|
| 363 |
+
add("overcorrection", "quran", "إنا أعطيناك الكوثر", "Quran — NEVER modify", "critical")
|
| 364 |
+
add("overcorrection", "hadith", "إنما الأعمال بالنيات", "Hadith — NEVER modify", "critical")
|
| 365 |
+
add("overcorrection", "poetry", "قفا نبك من ذكرى حبيب ومنزل", "Poetry — preserve", "major")
|
| 366 |
+
|
| 367 |
+
# ══════════════════════════════════════════════════════════
|
| 368 |
+
# 80. UNDERCORRECTION — ERRORS THAT SHOULD BE CAUGHT
|
| 369 |
+
# ══════════════════════════════════════════════════════════
|
| 370 |
+
add("undercorrection", "hamza_missed", "اسلام عليكم", "إسلام — hamza missing", "major")
|
| 371 |
+
add("undercorrection", "ta_marbuta_missed", "الطبيعه جميله جدا", "Three errors — all should be caught", "major")
|
| 372 |
+
add("undercorrection", "double_error", "انا ذهبت الي الجامعه", "Two errors in one sentence", "major")
|
| 373 |
+
add("undercorrection", "grammar_missed", "الطلاب ذهب", "Subject-verb disagreement missed?", "major")
|
| 374 |
+
|
| 375 |
+
# ══════════════════════════════════════════════════════════
|
| 376 |
+
# 90. EDGE CASES
|
| 377 |
+
# ══════════════════════════════════════════════════════════
|
| 378 |
+
add("edge_case", "empty", "", "Should return error/empty", "major")
|
| 379 |
+
add("edge_case", "whitespace", " \t\n ", "Should return error/empty", "major")
|
| 380 |
+
add("edge_case", "single_char", "ا", "Should handle gracefully", "minor")
|
| 381 |
+
add("edge_case", "single_word", "مدرسة", "Single correct word — no change", "major")
|
| 382 |
+
add("edge_case", "very_long", "ا " * 2500, "5000 chars — no crash", "major")
|
| 383 |
+
add("edge_case", "html_injection", "<script>alert('xss')</script> مرحبا", "HTML stripped", "critical")
|
| 384 |
+
add("edge_case", "only_english", "Hello world this is a test", "Rejected — non-Arabic", "major")
|
| 385 |
+
add("edge_case", "emoji", "مرحبا 😊 كيف حالك 🎉", "Emoji preserved", "minor")
|
| 386 |
+
add("edge_case", "numbers_only", "123456789", "No crash", "minor")
|
| 387 |
+
add("edge_case", "repeated_chars", "كتاااااااااااب", "Collapse to كتاب", "major")
|
| 388 |
+
add("edge_case", "newlines", "السطر الأول\nالسطر الثاني\nالسطر الثالث", "Multi-line handling", "major")
|
| 389 |
+
add("edge_case", "unicode_special", "بسم\u200cالله", "Zero-width non-joiner", "minor")
|
| 390 |
+
add("edge_case", "diacritics", "كَتَبَ الطَّالِبُ الدَّرسَ", "Diacritized text — handle gracefully", "major")
|
| 391 |
+
add("edge_case", "punctuation_heavy", "!!!???...،،،؛؛؛", "Heavy punctuation — no crash", "minor")
|
| 392 |
+
|
| 393 |
+
# ══════════════════════════════════════════════════════════
|
| 394 |
+
# 100. SOCIAL MEDIA / INFORMAL
|
| 395 |
+
# ══════════════════════════════════════════════════════════
|
| 396 |
+
add("social_media", "informal", "كيفك شو اخبارك", "Dialect — graceful handling", "minor")
|
| 397 |
+
add("social_media", "informal", "يلا نروح", "Dialect — no crash", "minor")
|
| 398 |
+
add("social_media", "slang", "اخخخخ مش قادر", "Repeated chars + dialect", "minor")
|
| 399 |
+
|
| 400 |
+
# ══════════════════════════════════════════════════════════
|
| 401 |
+
# 110. APPLY-ALL SAFETY
|
| 402 |
+
# ══════════════════════════════════════════════════════════
|
| 403 |
+
add("apply_all", "no_duplicate", "انا ذهبت الي المدرسه",
|
| 404 |
+
"Apply-all must not duplicate words or lose spaces", "critical")
|
| 405 |
+
add("apply_all", "preserve_unchanged", "النص الأول صحيح ولكن الجامعه خطأ",
|
| 406 |
+
"Unchanged text must be preserved exactly", "critical")
|
| 407 |
+
|
| 408 |
+
# ══════════════════════════════════════════════════════════
|
| 409 |
+
# 120. CONCURRENCY / TIMING
|
| 410 |
+
# ══════════════════════════════════════════════════════════
|
| 411 |
+
add("concurrency", "rapid_fire", "انا طالب", "3 rapid requests — no crash", "major")
|
| 412 |
+
|
| 413 |
+
# ══════════════════════════════════════════════════════════
|
| 414 |
+
# 130. RELIGIOUS TEXT PROTECTION
|
| 415 |
+
# ══════════════════════════════════════════════════════════
|
| 416 |
+
add("religious", "quran", "بسم الله الرحمن الرحيم", "Must NOT be modified at all", "critical")
|
| 417 |
+
add("religious", "quran", "الحمد لله رب العالمين الرحمن الرحيم مالك يوم الدين",
|
| 418 |
+
"Al-Fatiha — must NOT be modified", "critical")
|
| 419 |
+
add("religious", "quran", "قل أعوذ برب الفلق من شر ما خلق",
|
| 420 |
+
"Surat Al-Falaq — must NOT be modified", "critical")
|
| 421 |
+
add("religious", "shahada", "لا إله إلا الله محمد رسول الله",
|
| 422 |
+
"Shahada — must NOT be modified", "critical")
|
| 423 |
+
|
| 424 |
+
# ══════════════════════════════════════════════════════════
|
| 425 |
+
# 140. DATES / TECHNICAL FORMATS
|
| 426 |
+
# ══════════════════════════════════════════════════════════
|
| 427 |
+
add("technical", "date", "تاريخ اليوم 15/06/2026", "Date format preserved", "major")
|
| 428 |
+
add("technical", "phone", "اتصل بالرقم 0123456789", "Phone number preserved", "major")
|
| 429 |
+
add("technical", "measurement", "المسافة 25.5 كم", "Decimal preserved", "major")
|
| 430 |
+
|
| 431 |
+
# ══════════════════════════════════════════════════════════
|
| 432 |
+
# 150. LONG TEXT
|
| 433 |
+
# ══════════════════════════════════════════════════════════
|
| 434 |
+
long_text = ("كان ياما كان في قديم الزمان ملك عظيم يحكم مملكه واسعه " * 10).strip()
|
| 435 |
+
add("stress", "long_500words", long_text, "No timeout, no crash", "major")
|
| 436 |
+
|
| 437 |
+
medium_text = ("الطلاب ذهبوا إلى المدرسة والمعلمون استقبلوهم بحرارة " * 20).strip()
|
| 438 |
+
add("stress", "medium_correct", medium_text, "Mostly correct — minimal changes", "major")
|
| 439 |
+
|
| 440 |
+
return tests
|
| 441 |
+
|
| 442 |
+
|
| 443 |
+
# ─── Test Runner ──────────────────────────────────────────────────────────────
|
| 444 |
+
|
| 445 |
+
def run_test(api: BayanAPI, tc: TestCase) -> TestResult:
|
| 446 |
+
"""Run a single test case and return the result."""
|
| 447 |
+
result = TestResult(
|
| 448 |
+
test_id=tc.id,
|
| 449 |
+
category=tc.category,
|
| 450 |
+
subcategory=tc.subcategory,
|
| 451 |
+
input_text=tc.input_text[:200],
|
| 452 |
+
expected_behavior=tc.expected_behavior,
|
| 453 |
+
severity=tc.severity,
|
| 454 |
+
status="error",
|
| 455 |
+
)
|
| 456 |
+
|
| 457 |
+
try:
|
| 458 |
+
# Choose endpoint based on category
|
| 459 |
+
if tc.category == "spelling":
|
| 460 |
+
resp = api.analyze(tc.input_text)
|
| 461 |
+
elif tc.category == "grammar":
|
| 462 |
+
resp = api.analyze(tc.input_text)
|
| 463 |
+
elif tc.category == "punctuation":
|
| 464 |
+
resp = api.analyze(tc.input_text)
|
| 465 |
+
elif tc.category in ("pipeline", "span", "morphology", "overcorrection",
|
| 466 |
+
"undercorrection", "apply_all", "religious", "technical",
|
| 467 |
+
"stress", "cross_model"):
|
| 468 |
+
resp = api.analyze(tc.input_text)
|
| 469 |
+
elif tc.category == "edge_case":
|
| 470 |
+
resp = api.analyze(tc.input_text)
|
| 471 |
+
elif tc.category == "concurrency":
|
| 472 |
+
resp = api.analyze(tc.input_text)
|
| 473 |
+
elif tc.category == "social_media":
|
| 474 |
+
resp = api.analyze(tc.input_text)
|
| 475 |
+
else:
|
| 476 |
+
resp = api.analyze(tc.input_text)
|
| 477 |
+
|
| 478 |
+
result.latency_ms = resp.get('_latency_ms', 0)
|
| 479 |
+
|
| 480 |
+
if 'error' in resp:
|
| 481 |
+
# Errors on edge cases like empty text are expected
|
| 482 |
+
if tc.subcategory in ('empty', 'whitespace'):
|
| 483 |
+
result.status = "pass"
|
| 484 |
+
result.actual_output = f"Error (expected): {resp['error']}"
|
| 485 |
+
else:
|
| 486 |
+
result.status = "error"
|
| 487 |
+
result.error_detail = resp['error']
|
| 488 |
+
return result
|
| 489 |
+
|
| 490 |
+
result.corrected_text = resp.get('corrected', '')
|
| 491 |
+
result.suggestions = resp.get('suggestions', [])
|
| 492 |
+
result.actual_output = result.corrected_text[:300]
|
| 493 |
+
|
| 494 |
+
# ── Validation Logic ──
|
| 495 |
+
original = resp.get('original', tc.input_text)
|
| 496 |
+
corrected = result.corrected_text
|
| 497 |
+
suggestions = result.suggestions
|
| 498 |
+
|
| 499 |
+
# --- Span alignment validation ---
|
| 500 |
+
if tc.category == "span" or True: # Always validate spans
|
| 501 |
+
for s in suggestions:
|
| 502 |
+
start = s.get('start', 0)
|
| 503 |
+
end = s.get('end', 0)
|
| 504 |
+
orig_text = s.get('original', '')
|
| 505 |
+
actual_slice = original[start:end]
|
| 506 |
+
if actual_slice != orig_text and orig_text:
|
| 507 |
+
result.status = "fail"
|
| 508 |
+
result.finding = (
|
| 509 |
+
f"SPAN MISMATCH: suggestion says original='{orig_text}' "
|
| 510 |
+
f"but text[{start}:{end}]='{actual_slice}'"
|
| 511 |
+
)
|
| 512 |
+
return result
|
| 513 |
+
|
| 514 |
+
# --- Overcorrection detection ---
|
| 515 |
+
if tc.category == "overcorrection" or tc.category == "religious":
|
| 516 |
+
if corrected != original and suggestions:
|
| 517 |
+
result.status = "fail"
|
| 518 |
+
result.finding = (
|
| 519 |
+
f"OVERCORRECTION: Correct text was modified. "
|
| 520 |
+
f"Changes: {[s.get('original','')+'→'+s.get('correction','') for s in suggestions]}"
|
| 521 |
+
)
|
| 522 |
+
return result
|
| 523 |
+
|
| 524 |
+
# --- Spelling false positive (correct text changed) ---
|
| 525 |
+
if tc.subcategory == "overcorrection" and tc.category == "spelling":
|
| 526 |
+
if corrected != original:
|
| 527 |
+
result.status = "fail"
|
| 528 |
+
result.finding = (
|
| 529 |
+
f"SPELLING FALSE POSITIVE: '{original[:80]}' was changed to '{corrected[:80]}'"
|
| 530 |
+
)
|
| 531 |
+
return result
|
| 532 |
+
|
| 533 |
+
# --- Grammar overcorrection ---
|
| 534 |
+
if tc.subcategory == "overcorrection" and tc.category == "grammar":
|
| 535 |
+
if corrected != original:
|
| 536 |
+
result.status = "fail"
|
| 537 |
+
result.finding = (
|
| 538 |
+
f"GRAMMAR FALSE POSITIVE: '{original[:80]}' was changed to '{corrected[:80]}'"
|
| 539 |
+
)
|
| 540 |
+
return result
|
| 541 |
+
|
| 542 |
+
# --- Numeral protection ---
|
| 543 |
+
if tc.subcategory == "numerals":
|
| 544 |
+
orig_digits = re.findall(r'\d+', original)
|
| 545 |
+
corr_digits = re.findall(r'\d+', corrected)
|
| 546 |
+
if orig_digits != corr_digits:
|
| 547 |
+
result.status = "fail"
|
| 548 |
+
result.finding = f"NUMERAL CORRUPTION: {orig_digits} → {corr_digits}"
|
| 549 |
+
return result
|
| 550 |
+
|
| 551 |
+
# --- Pronoun suffix guard ---
|
| 552 |
+
if tc.subcategory == "pronoun_suffix":
|
| 553 |
+
for s in suggestions:
|
| 554 |
+
if 'ته' in s.get('original', '') and 'تة' in s.get('correction', ''):
|
| 555 |
+
result.status = "fail"
|
| 556 |
+
result.finding = f"PRONOUN SUFFIX LEAK: {s['original']}→{s['correction']}"
|
| 557 |
+
return result
|
| 558 |
+
|
| 559 |
+
# --- Apply-all safety ---
|
| 560 |
+
if tc.category == "apply_all":
|
| 561 |
+
# Simulate apply-all
|
| 562 |
+
rebuilt = original
|
| 563 |
+
for s in sorted(suggestions, key=lambda x: -x['start']):
|
| 564 |
+
rebuilt = rebuilt[:s['start']] + s['correction'] + rebuilt[s['end']:]
|
| 565 |
+
if rebuilt != corrected:
|
| 566 |
+
result.status = "fail"
|
| 567 |
+
result.finding = (
|
| 568 |
+
f"APPLY-ALL MISMATCH: rebuilt='{rebuilt[:100]}' vs corrected='{corrected[:100]}'"
|
| 569 |
+
)
|
| 570 |
+
return result
|
| 571 |
+
|
| 572 |
+
# --- HTML injection ---
|
| 573 |
+
if tc.subcategory == "html_injection":
|
| 574 |
+
if '<script>' in corrected or '<' in corrected:
|
| 575 |
+
result.status = "fail"
|
| 576 |
+
result.finding = "HTML NOT STRIPPED"
|
| 577 |
+
return result
|
| 578 |
+
|
| 579 |
+
# --- Non-Arabic rejection ---
|
| 580 |
+
if tc.subcategory == "only_english":
|
| 581 |
+
if suggestions:
|
| 582 |
+
result.status = "fail"
|
| 583 |
+
result.finding = f"Non-Arabic text produced {len(suggestions)} suggestions"
|
| 584 |
+
return result
|
| 585 |
+
|
| 586 |
+
result.status = "pass"
|
| 587 |
+
|
| 588 |
+
except requests.Timeout:
|
| 589 |
+
result.status = "error"
|
| 590 |
+
result.error_detail = "TIMEOUT"
|
| 591 |
+
except Exception as e:
|
| 592 |
+
result.status = "error"
|
| 593 |
+
result.error_detail = f"{type(e).__name__}: {str(e)[:200]}"
|
| 594 |
+
|
| 595 |
+
return result
|
| 596 |
+
|
| 597 |
+
|
| 598 |
+
# ─── Main ─────────────────────────────────────────────────────────────────────
|
| 599 |
+
|
| 600 |
+
def main():
|
| 601 |
+
parser = argparse.ArgumentParser(description="Bayan Phase 8 Adversarial Audit")
|
| 602 |
+
parser.add_argument("--url", default=DEFAULT_URL, help="API base URL")
|
| 603 |
+
parser.add_argument("--out", default="phase8_audit_results.json", help="Output file")
|
| 604 |
+
parser.add_argument("--categories", nargs="*", help="Filter by categories")
|
| 605 |
+
args = parser.parse_args()
|
| 606 |
+
|
| 607 |
+
api = BayanAPI(args.url)
|
| 608 |
+
print(f"[AUDIT] Target: {args.url}")
|
| 609 |
+
|
| 610 |
+
# Health check
|
| 611 |
+
try:
|
| 612 |
+
health = api.health()
|
| 613 |
+
print(f"[AUDIT] Health: {json.dumps(health, indent=2)}")
|
| 614 |
+
except Exception as e:
|
| 615 |
+
print(f"[AUDIT] ❌ Health check failed: {e}")
|
| 616 |
+
print(f"[AUDIT] Continuing anyway...")
|
| 617 |
+
|
| 618 |
+
# Build dataset
|
| 619 |
+
tests = build_adversarial_dataset()
|
| 620 |
+
if args.categories:
|
| 621 |
+
tests = [t for t in tests if t.category in args.categories]
|
| 622 |
+
print(f"[AUDIT] Running {len(tests)} test cases...")
|
| 623 |
+
|
| 624 |
+
results = []
|
| 625 |
+
pass_count = 0
|
| 626 |
+
fail_count = 0
|
| 627 |
+
error_count = 0
|
| 628 |
+
|
| 629 |
+
for i, tc in enumerate(tests):
|
| 630 |
+
print(f" [{i+1}/{len(tests)}] {tc.id} {tc.category}/{tc.subcategory}: ", end="", flush=True)
|
| 631 |
+
r = run_test(api, tc)
|
| 632 |
+
results.append(asdict(r))
|
| 633 |
+
|
| 634 |
+
if r.status == "pass":
|
| 635 |
+
print(f"✅ ({r.latency_ms}ms)")
|
| 636 |
+
pass_count += 1
|
| 637 |
+
elif r.status == "fail":
|
| 638 |
+
print(f"❌ {r.finding[:80]}")
|
| 639 |
+
fail_count += 1
|
| 640 |
+
else:
|
| 641 |
+
print(f"⚠️ {r.error_detail[:80]}")
|
| 642 |
+
error_count += 1
|
| 643 |
+
|
| 644 |
+
# Summary
|
| 645 |
+
print(f"\n{'='*60}")
|
| 646 |
+
print(f"[AUDIT COMPLETE]")
|
| 647 |
+
print(f" Total: {len(results)}")
|
| 648 |
+
print(f" Pass: {pass_count}")
|
| 649 |
+
print(f" Fail: {fail_count}")
|
| 650 |
+
print(f" Error: {error_count}")
|
| 651 |
+
print(f"{'='*60}")
|
| 652 |
+
|
| 653 |
+
# Critical failures
|
| 654 |
+
critical_fails = [r for r in results if r['status'] == 'fail' and r['severity'] == 'critical']
|
| 655 |
+
if critical_fails:
|
| 656 |
+
print(f"\n🚨 CRITICAL FAILURES ({len(critical_fails)}):")
|
| 657 |
+
for r in critical_fails:
|
| 658 |
+
print(f" {r['test_id']} [{r['category']}/{r['subcategory']}]: {r['finding'][:100]}")
|
| 659 |
+
|
| 660 |
+
# Save results
|
| 661 |
+
output = {
|
| 662 |
+
"audit_timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
| 663 |
+
"target_url": args.url,
|
| 664 |
+
"total_tests": len(results),
|
| 665 |
+
"pass": pass_count,
|
| 666 |
+
"fail": fail_count,
|
| 667 |
+
"error": error_count,
|
| 668 |
+
"critical_failures": len(critical_fails) if critical_fails else 0,
|
| 669 |
+
"results": results,
|
| 670 |
+
}
|
| 671 |
+
|
| 672 |
+
with open(args.out, 'w', encoding='utf-8') as f:
|
| 673 |
+
json.dump(output, f, ensure_ascii=False, indent=2)
|
| 674 |
+
print(f"\n[AUDIT] Results saved to {args.out}")
|
| 675 |
+
|
| 676 |
+
|
| 677 |
+
if __name__ == "__main__":
|
| 678 |
+
main()
|
tests/phase9_results.json
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
tests/phase9_validation.py
ADDED
|
@@ -0,0 +1,811 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
BAYAN Phase 9 — Scientific Validation & Adversarial Benchmarking
|
| 3 |
+
=================================================================
|
| 4 |
+
Tests each model INDEPENDENTLY + full pipeline.
|
| 5 |
+
Produces precision/recall/F1 metrics with real API responses.
|
| 6 |
+
|
| 7 |
+
Usage:
|
| 8 |
+
python tests/phase9_validation.py --url URL [--phase A|B|C|D|E|ALL]
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import argparse, json, time, re, sys, os
|
| 12 |
+
import requests
|
| 13 |
+
from dataclasses import dataclass, field, asdict
|
| 14 |
+
from typing import List, Optional, Dict, Tuple
|
| 15 |
+
|
| 16 |
+
# ─── Config ───────────────────────────────────────────────────────────────────
|
| 17 |
+
DEFAULT_URL = "https://bayan10-bayan-api.hf.space"
|
| 18 |
+
|
| 19 |
+
# ─── API Client ───────────────────────────────────────────────────────────────
|
| 20 |
+
class API:
|
| 21 |
+
def __init__(self, base):
|
| 22 |
+
self.base = base.rstrip('/')
|
| 23 |
+
self.s = requests.Session()
|
| 24 |
+
self.s.headers['Content-Type'] = 'application/json'
|
| 25 |
+
|
| 26 |
+
def _post(self, endpoint, payload, timeout=180):
|
| 27 |
+
t0 = time.time()
|
| 28 |
+
try:
|
| 29 |
+
r = self.s.post(f"{self.base}{endpoint}", json=payload, timeout=timeout)
|
| 30 |
+
ms = int((time.time()-t0)*1000)
|
| 31 |
+
d = r.json(); d['_ms'] = ms; d['_status'] = r.status_code
|
| 32 |
+
return d
|
| 33 |
+
except requests.Timeout:
|
| 34 |
+
return {'error': 'TIMEOUT', '_ms': int((time.time()-t0)*1000), '_status': 0}
|
| 35 |
+
except Exception as e:
|
| 36 |
+
return {'error': str(e), '_ms': int((time.time()-t0)*1000), '_status': 0}
|
| 37 |
+
|
| 38 |
+
def health(self): return self._post('/api/health', {})
|
| 39 |
+
def spelling(self, text): return self._post('/api/spelling', {'text': text})
|
| 40 |
+
def grammar(self, text): return self._post('/api/grammar', {'text': text})
|
| 41 |
+
def punctuation(self, text): return self._post('/api/punctuation', {'text': text})
|
| 42 |
+
def analyze(self, text): return self._post('/api/analyze', {'text': text})
|
| 43 |
+
def summarize(self, text): return self._post('/api/summarize', {'text': text, 'length': 'short'})
|
| 44 |
+
def dialect(self, text): return self._post('/api/dialect', {'text': text})
|
| 45 |
+
def autocomplete(self, text): return self._post('/api/autocomplete', {'text': text, 'n': 5})
|
| 46 |
+
|
| 47 |
+
# ─── Test Case ────────────────────────────────────────────────────────────────
|
| 48 |
+
@dataclass
|
| 49 |
+
class TC:
|
| 50 |
+
id: str
|
| 51 |
+
phase: str
|
| 52 |
+
category: str
|
| 53 |
+
input: str
|
| 54 |
+
expected_output: str = ""
|
| 55 |
+
should_change: bool = True # True=error should be fixed, False=correct text, no change
|
| 56 |
+
error_words: list = field(default_factory=list) # words that should be corrected
|
| 57 |
+
correct_words: list = field(default_factory=list) # words that must NOT change
|
| 58 |
+
|
| 59 |
+
@dataclass
|
| 60 |
+
class Result:
|
| 61 |
+
tc_id: str; phase: str; category: str
|
| 62 |
+
input: str; expected: str
|
| 63 |
+
actual_output: str = ""
|
| 64 |
+
changed: bool = False
|
| 65 |
+
suggestions: list = field(default_factory=list)
|
| 66 |
+
latency_ms: int = 0
|
| 67 |
+
verdict: str = "" # TP, FP, TN, FN, ERROR
|
| 68 |
+
detail: str = ""
|
| 69 |
+
api_status: int = 0
|
| 70 |
+
raw_response: dict = field(default_factory=dict)
|
| 71 |
+
|
| 72 |
+
# ═══════════════════════════════════════════════════════════════════════════════
|
| 73 |
+
# PHASE A — RAW SPELLING MODEL VALIDATION
|
| 74 |
+
# ═══════════════════════════════════════════════════════════════════════════════
|
| 75 |
+
def build_spelling_tests() -> List[TC]:
|
| 76 |
+
T = []
|
| 77 |
+
n = [0]
|
| 78 |
+
def add(cat, inp, exp, should_change=True, err=None, correct=None):
|
| 79 |
+
n[0]+=1
|
| 80 |
+
T.append(TC(f"A{n[0]:03d}", "A", cat, inp, exp, should_change,
|
| 81 |
+
err or [], correct or []))
|
| 82 |
+
|
| 83 |
+
# ── A1: Hamza errors (SHOULD be corrected) ──
|
| 84 |
+
add("hamza", "انا طالب في الجامعة", "أنا طالب في الجامعة", True, ["انا"])
|
| 85 |
+
add("hamza", "اذا جاء الربيع تزهر الأشجار", "إذا جاء الربيع تزهر الأشجار", True, ["اذا"])
|
| 86 |
+
add("hamza", "ايضا هذا الأمر مهم جداً", "أيضاً هذا الأمر مهم جداً", True, ["ايضا"])
|
| 87 |
+
add("hamza", "لان الأمر يتعلق بالمستقبل", "لأن الأمر يتعلق بالمستقبل", True, ["لان"])
|
| 88 |
+
add("hamza", "اين ذهبت أمس", "أين ذهبت أمس", True, ["اين"])
|
| 89 |
+
add("hamza", "اول مرة أزور هذا المكان", "أول مرة أزور هذا المكان", True, ["اول"])
|
| 90 |
+
add("hamza", "هذا او ذاك لا فرق", "هذا أو ذاك لا فرق", True, ["ا��"])
|
| 91 |
+
add("hamza", "اكبر مدينة في العالم", "أكبر مدينة في العالم", True, ["اكبر"])
|
| 92 |
+
add("hamza", "اصغر طالب في الصف", "أصغر طالب في الصف", True, ["اصغر"])
|
| 93 |
+
add("hamza", "ابناء الوطن يعملون بجد", "أبناء الوطن يعملون بجد", True, ["ابناء"])
|
| 94 |
+
add("hamza", "اطفال المدرسة يلعبون", "أطفال المدرسة يلعبون", True, ["اطفال"])
|
| 95 |
+
add("hamza", "اخيراً وصلنا إلى الهدف", "أخيراً وصلنا إلى الهدف", True, ["اخيراً"])
|
| 96 |
+
add("hamza", "وقف امام المدرسة", "وقف أمام المدرسة", True, ["امام"])
|
| 97 |
+
# Prefixed hamza
|
| 98 |
+
add("hamza_prefix", "والاسعار مرتفعة جداً", "والأسعار مرتفعة جداً", True, ["والاسعار"])
|
| 99 |
+
add("hamza_prefix", "بالاضافة إلى ذلك", "بالإضافة إلى ذلك", True, ["بالاضافة"])
|
| 100 |
+
add("hamza_prefix", "فالانسان يحتاج للعلم", "فالإنسان يحتاج للعلم", True, ["فالانسان"])
|
| 101 |
+
|
| 102 |
+
# ── A2: Ta Marbuta errors ──
|
| 103 |
+
add("ta_marbuta", "المدرسه كبيره وجميله", "المدرسة كبيرة وجميلة", True, ["المدرسه","كبيره","جميله"])
|
| 104 |
+
add("ta_marbuta", "الجامعه في القاهره", "الجامعة في القاهرة", True, ["الجامعه","القاهره"])
|
| 105 |
+
add("ta_marbuta", "السياره سريعه جداً", "السيارة سريعة جداً", True, ["السياره","سريعه"])
|
| 106 |
+
add("ta_marbuta", "الشجره طويله", "الشجرة طويلة", True, ["الشجره","طويله"])
|
| 107 |
+
add("ta_marbuta", "الحياه صعبه في المدينه", "الحياة صعبة في المدينة", True, ["الحياه","صعبه","المدينه"])
|
| 108 |
+
add("ta_marbuta", "بالمدرسه الكبيره", "بالمدرسة الكبيرة", True, ["بالمدرسه","الكبيره"])
|
| 109 |
+
|
| 110 |
+
# ── A3: Alif Maqsura ──
|
| 111 |
+
add("alif_maqsura", "ذهبت الي المكتبة", "ذهبت إلى المكتبة", True, ["الي"])
|
| 112 |
+
add("alif_maqsura", "المستشفي الكبير", "المستشفى الكبير", True, ["المستشفي"])
|
| 113 |
+
add("alif_maqsura", "هدي الطالبة ممتاز", "هدى الطالبة ممتاز", True, ["هدي"])
|
| 114 |
+
|
| 115 |
+
# ── A4: Word Splits ──
|
| 116 |
+
add("word_split", "ذهبت فيالبيت", "ذهبت في البيت", True, ["فيالبيت"])
|
| 117 |
+
add("word_split", "خرج منالمدرسة", "خرج من المدرسة", True, ["منالمدرسة"])
|
| 118 |
+
add("word_split", "بقي عندالباب", "بقي عند الباب", True, ["عندالباب"])
|
| 119 |
+
|
| 120 |
+
# ── A5: Correct text — MUST NOT change (overcorrection tests) ──
|
| 121 |
+
add("correct_text", "أنا ذهبت إلى الجامعة", "أنا ذهبت إلى الجامعة", False, correct=["أنا","ذهبت","إلى","الجامعة"])
|
| 122 |
+
add("correct_text", "هذه المدرسة جميلة جداً", "هذه المدرسة جميلة جداً", False, correct=["هذه","المدرسة","جميلة"])
|
| 123 |
+
add("correct_text", "كان الجو ممطراً اليوم", "كان الجو ممطراً اليوم", False, correct=["كان"])
|
| 124 |
+
add("correct_text", "وكان أحمد في المنزل", "وكان أحمد في المنزل", False, correct=["وكان"])
|
| 125 |
+
add("correct_text", "إلى اللقاء يا صديقي", "إلى اللقاء يا صديقي", False, correct=["إلى"])
|
| 126 |
+
add("correct_text", "ذلك الكتاب مفيد جداً", "ذلك الكتاب مفيد جداً", False, correct=["ذلك"])
|
| 127 |
+
add("correct_text", "لكن الأمر صعب علينا", "لكن الأمر صعب علينا", False, correct=["لكن"])
|
| 128 |
+
add("correct_text", "هذا أو ذاك سواء عندي", "هذا أو ذاك سواء عندي", False, correct=["أو"])
|
| 129 |
+
|
| 130 |
+
# ── A6: Pronoun suffix guard ──
|
| 131 |
+
add("pronoun_guard", "فتأملته جيداً في المساء", "فتأملته جيداً في المساء", False, correct=["فتأملته"])
|
| 132 |
+
add("pronoun_guard", "رأيته في الشارع أمس", "رأيته في الشارع أمس", False, correct=["رأيته"])
|
| 133 |
+
add("pronoun_guard", "كتبته بسرعة كبيرة", "كتبته بسرعة كبيرة", False, correct=["كتبته"])
|
| 134 |
+
add("pronoun_guard", "سمعته يتحدث بوضوح", "سمعته يتحدث بوضوح", False, correct=["سمعته"])
|
| 135 |
+
|
| 136 |
+
# ── A7: Named Entities ──
|
| 137 |
+
add("named_entity", "محمد صلاح لاعب كرة قدم مصري", "", False, correct=["محمد","صلاح"])
|
| 138 |
+
add("named_entity", "جامعة القاهرة من أعرق الجامعات", "", False, correct=["القاهرة"])
|
| 139 |
+
add("named_entity", "مدينة الرياض عاصمة المملكة", "", False, correct=["الرياض"])
|
| 140 |
+
add("named_entity", "عبدالله يدرس في الجامعة", "", False, correct=["عبدالله"])
|
| 141 |
+
|
| 142 |
+
# ── A8: Numbers ──
|
| 143 |
+
add("numbers", "عام 2024 كان جيداً جداً", "", False, correct=["2024"])
|
| 144 |
+
add("numbers", "اشتريت 15 كتاباً من المعرض", "", False, correct=["15"])
|
| 145 |
+
add("numbers", "الساعة 3:30 مساءً بالضبط", "", False, correct=["3:30"])
|
| 146 |
+
|
| 147 |
+
# ── A9: Technical / Foreign ──
|
| 148 |
+
add("foreign", "أستخدم Python في البرمجة", "", False, correct=["Python"])
|
| 149 |
+
add("foreign", "تطبيق OpenAI ممتاز جداً", "", False, correct=["OpenAI"])
|
| 150 |
+
add("foreign", "خادم Docker يعمل بنجاح", "", False, correct=["Docker"])
|
| 151 |
+
add("foreign", "إطار TensorFlow مفيد للتعلم", "", False, correct=["TensorFlow"])
|
| 152 |
+
|
| 153 |
+
# ── A10: Mixed Arabic/English ──
|
| 154 |
+
add("mixed", "البريد user@example.com مهم جداً", "", False, correct=["user@example.com"])
|
| 155 |
+
add("mixed", "الرابط https://google.com يعمل", "", False, correct=["https://google.com"])
|
| 156 |
+
add("mixed", "الهاشتاق #الذكاء_الاصطناعي مهم", "", False, correct=["#الذكاء_الاصطناعي"])
|
| 157 |
+
|
| 158 |
+
# ── A11: Religious text — MUST NOT change ──
|
| 159 |
+
add("religious", "بسم الله الرحمن الرحيم", "بسم الله الرحمن الرحيم", False, correct=["بسم","الله","الرحمن","الرحيم"])
|
| 160 |
+
add("religious", "الحمد لله رب العالمين", "الحمد لله رب العالمين", False, correct=["الحمد","لله","رب","العالمين"])
|
| 161 |
+
add("religious", "لا إله إلا الله محمد رسول الله", "", False, correct=["إله","إلا","الله","محمد","رسول"])
|
| 162 |
+
add("religious", "إنما الأعمال بالنيات", "", False, correct=["إنما","الأعمال","بالنيات"])
|
| 163 |
+
|
| 164 |
+
# ── A12: Repeated chars ──
|
| 165 |
+
add("repeated", "كتاااااب جميييل", "كتاب جميل", True, ["كتاااااب","جميييل"])
|
| 166 |
+
|
| 167 |
+
# ── A13: Edge cases ──
|
| 168 |
+
add("edge", "مدرسة", "مدرسة", False, correct=["مدرسة"])
|
| 169 |
+
add("edge", "ا ب ت ث ج ح خ", "", False)
|
| 170 |
+
add("edge", "😊 مرحبا 🎉 كيف حالك", "", False)
|
| 171 |
+
|
| 172 |
+
return T
|
| 173 |
+
|
| 174 |
+
# ═══════════════════════════════════════════════════════════════════════════════
|
| 175 |
+
# PHASE B — RAW GRAMMAR MODEL VALIDATION
|
| 176 |
+
# ═══════════════════════════════════════════════════════════════════════════════
|
| 177 |
+
def build_grammar_tests() -> List[TC]:
|
| 178 |
+
T = []
|
| 179 |
+
n = [0]
|
| 180 |
+
def add(cat, inp, exp, should_change=True, err=None, correct=None):
|
| 181 |
+
n[0]+=1
|
| 182 |
+
T.append(TC(f"B{n[0]:03d}", "B", cat, inp, exp, should_change,
|
| 183 |
+
err or [], correct or []))
|
| 184 |
+
|
| 185 |
+
# ── B1: Subject-Verb Agreement (errors) ──
|
| 186 |
+
add("sv_agree", "البنات ذهب إلى المدرسة", "", True, ["ذهب"])
|
| 187 |
+
add("sv_agree", "الطلاب يذهب إلى الجامعة", "", True, ["يذهب"])
|
| 188 |
+
add("sv_agree", "المهندسون حضر الاجتماع", "", True, ["حضر"])
|
| 189 |
+
add("sv_agree", "الرجال يعمل في المصنع", "", True, ["يعمل"])
|
| 190 |
+
add("sv_agree", "النساء ذهب إلى السوق", "", True, ["ذهب"])
|
| 191 |
+
add("sv_agree", "الأولاد لعب في الحديقة", "", True, ["لعب"])
|
| 192 |
+
|
| 193 |
+
# ── B2: Gender Agreement (errors) ──
|
| 194 |
+
add("gender", "السيارة جميل والبيت كبير", "", True, ["جميل"])
|
| 195 |
+
add("gender", "البنت ذكي في المدرسة", "", True, ["ذكي"])
|
| 196 |
+
add("gender", "الطالبة متفوق في دراسته", "", True, ["متفوق"])
|
| 197 |
+
|
| 198 |
+
# ── B3: Preposition Case (errors) ──
|
| 199 |
+
add("case", "في المهندسون الماهرون جداً", "", True, ["المهندسون"])
|
| 200 |
+
add("case", "من المعلمون الأكفاء في المدرسة", "", True, ["المعلمون"])
|
| 201 |
+
add("case", "إلى المسافرون في المطار", "", True, ["المسافرون"])
|
| 202 |
+
add("case", "على العاملون في المصنع", "", True, ["العاملون"])
|
| 203 |
+
|
| 204 |
+
# ── B4: Five Nouns (errors) ──
|
| 205 |
+
add("five_nouns", "إن أبوك رجل طيب جداً", "", True, ["أبوك"])
|
| 206 |
+
add("five_nouns", "رأيت أخوك في المسجد أمس", "", True, ["أخوك"])
|
| 207 |
+
|
| 208 |
+
# ── B5: Dual Forms (errors) ──
|
| 209 |
+
add("dual", "هذان الطالبتان مجتهدتان", "", True, ["هذان"])
|
| 210 |
+
add("dual", "هاتان الطالبان مجتهدان", "", True, ["هاتان"])
|
| 211 |
+
|
| 212 |
+
# ── B6: Nasb/Jazm (errors) ──
|
| 213 |
+
add("nasb", "لن يذهبون إلى المدرسة غداً", "", True, ["يذهبون"])
|
| 214 |
+
add("nasb", "لم يفعلون الواجب بعد", "", True, ["يفعلون"])
|
| 215 |
+
|
| 216 |
+
# ── B7: Correct grammar — MUST NOT change ──
|
| 217 |
+
add("correct", "ذهب الطالب إلى المدرسة", "", False, correct=["ذهب","الطالب"])
|
| 218 |
+
add("correct", "كتبت الطالبة المقال بنجاح", "", False, correct=["كتبت","الطالبة"])
|
| 219 |
+
add("correct", "المعلمون في المدرسة يعملون", "", False, correct=["المعلمون","يعملون"])
|
| 220 |
+
add("correct", "أحب القراءة والكتابة كثيراً", "", False, correct=["أحب","القراءة","والكتابة"])
|
| 221 |
+
add("correct", "ذهبت البنات إلى المدرسة", "", False, correct=["ذهبت","البنات"])
|
| 222 |
+
add("correct", "جاء المعلمون إلى الفصل", "", False, correct=["جاء","المعلمون"])
|
| 223 |
+
|
| 224 |
+
# ── B8: Quranic text — MUST NOT change ──
|
| 225 |
+
add("quran", "بسم الله الرحمن الرحيم", "", False, correct=["بسم","الله","الرحمن","الرحيم"])
|
| 226 |
+
add("quran", "قل هو الله أحد الله الصمد", "", False)
|
| 227 |
+
add("quran", "إنا أنزلناه في ليلة القدر", "", False)
|
| 228 |
+
add("quran", "قل أعوذ برب الفلق من شر ما خلق", "", False)
|
| 229 |
+
add("quran", "الحمد لله رب العالمين الرحمن الرحيم مالك يوم الدين", "", False)
|
| 230 |
+
|
| 231 |
+
# ── B9: Hadith — MUST NOT change ──
|
| 232 |
+
add("hadith", "إنما الأعمال بالنيات وإنما لكل امرئ ما نوى", "", False)
|
| 233 |
+
add("hadith", "خيركم من تعلم القرآن وعلمه", "", False)
|
| 234 |
+
|
| 235 |
+
# ── B10: Poetry — MUST NOT change ──
|
| 236 |
+
add("poetry", "قفا نبك من ذكرى حبيب ومنزل", "", False)
|
| 237 |
+
add("poetry", "على قدر أهل العزم تأتي العزائم", "", False)
|
| 238 |
+
|
| 239 |
+
# ── B11: Academic Arabic — MUST NOT change ──
|
| 240 |
+
add("academic", "إن الأبحاث العلمية تشير إلى أهمية التعليم في تطوير المجتمعات الحديثة", "", False)
|
| 241 |
+
add("academic", "أشارت الدراسة إلى أن نسبة النجاح بلغت خمسة وتسعين بالمئة", "", False)
|
| 242 |
+
add("academic", "تهدف هذه الدراسة إلى تحليل العوامل المؤثرة في جودة التعليم العالي", "", False)
|
| 243 |
+
|
| 244 |
+
# ── B12: News Arabic — MUST NOT change ──
|
| 245 |
+
add("news", "أعلن رئيس الوزراء عن خطة اقتصادية جديدة لتطوير البنية التحتية", "", False)
|
| 246 |
+
add("news", "شهدت المنطقة تطورات ميدانية متسارعة خلال الأيام الماضية", "", False)
|
| 247 |
+
|
| 248 |
+
return T
|
| 249 |
+
|
| 250 |
+
# ═══════════════════════════════════════════════════════════════════════════════
|
| 251 |
+
# PHASE C — RAW PUNCTUATION MODEL VALIDATION
|
| 252 |
+
# ═══════════════════════════════════════════════════════════════════════════════
|
| 253 |
+
def build_punctuation_tests() -> List[TC]:
|
| 254 |
+
T = []
|
| 255 |
+
n = [0]
|
| 256 |
+
def add(cat, inp, exp, should_change=True, err=None, correct=None):
|
| 257 |
+
n[0]+=1
|
| 258 |
+
T.append(TC(f"C{n[0]:03d}", "C", cat, inp, exp, should_change,
|
| 259 |
+
err or [], correct or []))
|
| 260 |
+
|
| 261 |
+
# ── C1: Missing punctuation (should add) ──
|
| 262 |
+
add("missing_period", "ذهبت إلى المدرسة ثم عدت إلى البيت", "", True)
|
| 263 |
+
add("missing_question", "هل أنت بخير يا صديقي", "", True)
|
| 264 |
+
add("missing_comma", "مرحبا كيف حالك اليوم", "", True)
|
| 265 |
+
add("missing_multi", "كيف حالك أنا بخير والحمد لله", "", True)
|
| 266 |
+
|
| 267 |
+
# ── C2: Already punctuated — MUST NOT over-punctuate ──
|
| 268 |
+
add("already_punct", "ذهبت إلى المدرسة. ثم عدت.", "", False)
|
| 269 |
+
add("already_punct", "كيف حالك؟ أنا بخير.", "", False)
|
| 270 |
+
add("already_punct", "أحمد، كيف حالك؟ هل أنت بخير؟", "", False)
|
| 271 |
+
|
| 272 |
+
# ── C3: Punctuation must NOT change words ──
|
| 273 |
+
add("no_word_change", "ذهبت الي المدرسه أمس", "", True)
|
| 274 |
+
# ^ Only add punct — must NOT fix الي→إلى or المدرسه→المدرسة
|
| 275 |
+
|
| 276 |
+
# ── C4: Position accuracy ──
|
| 277 |
+
add("position", "سألته كيف حالك فقال أنا بخير", "", True)
|
| 278 |
+
add("position", "ذهبت إلى المكتبة واشتريت كتاباً ثم عدت", "", True)
|
| 279 |
+
|
| 280 |
+
return T
|
| 281 |
+
|
| 282 |
+
# ═══════════════════════════════════════════════════════════════════════════════
|
| 283 |
+
# PHASE D — FULL PIPELINE VALIDATION
|
| 284 |
+
# ═══════════════════════════════════════���═══════════════════════════════════════
|
| 285 |
+
def build_pipeline_tests() -> List[TC]:
|
| 286 |
+
T = []
|
| 287 |
+
n = [0]
|
| 288 |
+
def add(cat, inp, exp="", should_change=True, err=None, correct=None):
|
| 289 |
+
n[0]+=1
|
| 290 |
+
T.append(TC(f"D{n[0]:03d}", "D", cat, inp, exp, should_change,
|
| 291 |
+
err or [], correct or []))
|
| 292 |
+
|
| 293 |
+
# ── D1: Multi-stage corrections ──
|
| 294 |
+
add("multi_stage", "انا ذهب الى الجامعه كيف حالك", "", True,
|
| 295 |
+
["انا","الى","الجامعه"])
|
| 296 |
+
add("multi_stage", "البنات ذهب الى المدرسه", "", True,
|
| 297 |
+
["ذهب","الى","المدرسه"])
|
| 298 |
+
add("multi_stage", "هي ذهب الي الجامعه", "", True,
|
| 299 |
+
["ذهب","الي","الجامعه"])
|
| 300 |
+
|
| 301 |
+
# ── D2: Correct text through pipeline ──
|
| 302 |
+
add("correct_pipeline", "أنا ذهبت إلى الجامعة.", "", False,
|
| 303 |
+
correct=["أنا","ذهبت","إلى","الجامعة"])
|
| 304 |
+
add("correct_pipeline", "ذهب الطالب إلى المدرسة.", "", False,
|
| 305 |
+
correct=["ذهب","الطالب","إلى","المدرسة"])
|
| 306 |
+
|
| 307 |
+
# ── D3: Cross-model conflict ──
|
| 308 |
+
add("cross_conflict", "الجامعه كبيره والطلاب كثيرون", "", True,
|
| 309 |
+
["الجامعه","كبيره"])
|
| 310 |
+
add("cross_conflict", "المدرسه جميله والمعلمون في الفصل", "", True,
|
| 311 |
+
["المدرسه","جميله"])
|
| 312 |
+
|
| 313 |
+
# ── D4: Span alignment after pipeline ──
|
| 314 |
+
add("span_align", "المدرسه كبيره جداً", "", True, ["المدرسه","كبيره"])
|
| 315 |
+
add("span_align", "انا في المدرسه الكبيره", "", True, ["انا","المدرسه","الكبيره"])
|
| 316 |
+
|
| 317 |
+
# ── D5: Religious text through pipeline ──
|
| 318 |
+
add("religious_pipeline", "بسم الله الرحمن الرحيم", "", False,
|
| 319 |
+
correct=["بسم","الله","الرحمن","الرحيم"])
|
| 320 |
+
add("religious_pipeline", "الحمد لله رب العالمين", "", False,
|
| 321 |
+
correct=["الحمد","لله","رب","العالمين"])
|
| 322 |
+
|
| 323 |
+
# ── D6: Apply-all safety ──
|
| 324 |
+
add("apply_all", "انا ذهبت الي المدرسه", "", True, ["انا","الي","المدرسه"])
|
| 325 |
+
add("apply_all", "النص الأول صحيح ولكن الجامعه خطأ", "", True, ["الجامعه"])
|
| 326 |
+
|
| 327 |
+
# ── D7: Long text ──
|
| 328 |
+
long = "هذا النص طويل جداً " * 20
|
| 329 |
+
add("long_text", long.strip(), "", False)
|
| 330 |
+
|
| 331 |
+
# ── D8: Edge cases ──
|
| 332 |
+
add("edge_empty", "", "", False)
|
| 333 |
+
add("edge_short", "مرحبا", "", False)
|
| 334 |
+
add("edge_html", "<script>alert('xss')</script> مرحبا بكم في الموقع", "", True)
|
| 335 |
+
add("edge_english", "Hello world this is a test of English text only", "", False)
|
| 336 |
+
|
| 337 |
+
return T
|
| 338 |
+
|
| 339 |
+
# ═══════════════════════════════════════════════════════════════════════════════
|
| 340 |
+
# PHASE E — ADVERSARIAL ATTACKS
|
| 341 |
+
# ═══════════════════════════════════════════════════════════════════════════════
|
| 342 |
+
def build_adversarial_tests() -> List[TC]:
|
| 343 |
+
T = []
|
| 344 |
+
n = [0]
|
| 345 |
+
def add(cat, inp, exp="", should_change=False, err=None, correct=None):
|
| 346 |
+
n[0]+=1
|
| 347 |
+
T.append(TC(f"E{n[0]:03d}", "E", cat, inp, exp, should_change,
|
| 348 |
+
err or [], correct or []))
|
| 349 |
+
|
| 350 |
+
# ── E1: Dialect ──
|
| 351 |
+
add("dialect", "ازيك عامل ايه انهارده", "", True)
|
| 352 |
+
add("dialect", "كيفك شو اخبارك اليوم", "", True)
|
| 353 |
+
add("dialect", "شلونك وين رايح", "", True)
|
| 354 |
+
|
| 355 |
+
# ── E2: Franco Arabic ──
|
| 356 |
+
add("franco", "ana ray7 el gam3a", "", False)
|
| 357 |
+
add("franco", "3ayz atkalem ma3ak", "", False)
|
| 358 |
+
|
| 359 |
+
# ── E3: Excessive repetition ──
|
| 360 |
+
add("repetition", "هههههههههه مضحك جداااااا", "", True)
|
| 361 |
+
add("repetition", "لاااااااا مش ممكن", "", True)
|
| 362 |
+
|
| 363 |
+
# ── E4: Emoji heavy ──
|
| 364 |
+
add("emoji", "😊😊😊 مرحبا 🎉🎉🎉 كيف حالك 🌟", "", False)
|
| 365 |
+
|
| 366 |
+
# ── E5: Mixed scripts ──
|
| 367 |
+
add("mixed_script", "I love القراءة and الكتابة", "", False)
|
| 368 |
+
add("mixed_script", "المشروع يستخدم React و Node.js", "", False)
|
| 369 |
+
|
| 370 |
+
# ── E6: Code ──
|
| 371 |
+
add("code", "print('مرحبا بالعالم')", "", False)
|
| 372 |
+
add("code", "function test() { return 'مرحبا'; }", "", False)
|
| 373 |
+
|
| 374 |
+
# ── E7: URLs and emails ──
|
| 375 |
+
add("url", "زر الموقع https://www.example.com/path?q=test للمزيد", "", False)
|
| 376 |
+
add("email", "أرسل لي على info@company.com رجاءً", "", False)
|
| 377 |
+
|
| 378 |
+
# ── E8: Numbers/dates ──
|
| 379 |
+
add("numbers", "تاريخ اليوم 15/06/2026 وا��ساعة 14:30", "", False)
|
| 380 |
+
add("numbers", "المسافة 25.5 كم والحرارة 35°C", "", False)
|
| 381 |
+
|
| 382 |
+
# ── E9: Unicode edge cases ──
|
| 383 |
+
add("unicode", "بسم\u200cالله", "", False) # ZWNJ
|
| 384 |
+
add("unicode", "مرحبا\u200bبكم", "", False) # ZWS
|
| 385 |
+
add("unicode", "كَتَبَ الطَّالِبُ الدَّرسَ", "", False) # Diacritics
|
| 386 |
+
|
| 387 |
+
# ── E10: Very long single word ──
|
| 388 |
+
add("long_word", "واستغفروالذنوبهمجميعاًفإنهم محتاجون", "", True)
|
| 389 |
+
|
| 390 |
+
# ── E11: Punctuation spam ──
|
| 391 |
+
add("punct_spam", "!!!???...،،،؛؛؛:::...!!!", "", False)
|
| 392 |
+
|
| 393 |
+
# ── E12: Newlines ──
|
| 394 |
+
add("newlines", "السطر الأول\nالسطر الثاني\nالسطر الثالث", "", False)
|
| 395 |
+
|
| 396 |
+
# ── E13: Hashtags/mentions ──
|
| 397 |
+
add("hashtag", "مشروع #بيان رائع جداً @mohamedatef", "", False, correct=["#بيان","@mohamedatef"])
|
| 398 |
+
|
| 399 |
+
return T
|
| 400 |
+
|
| 401 |
+
# ═══════════════════════════════════════════════════════════════════════════════
|
| 402 |
+
# RUNNER
|
| 403 |
+
# ═══════════════════════════════════════════════════════════════════════════════
|
| 404 |
+
|
| 405 |
+
def run_spelling_test(api: API, tc: TC) -> Result:
|
| 406 |
+
"""Test spelling model independently via /api/analyze (short text triggers spelling)."""
|
| 407 |
+
r = Result(tc.id, tc.phase, tc.category, tc.input, tc.expected_output)
|
| 408 |
+
resp = api.analyze(tc.input)
|
| 409 |
+
r.api_status = resp.get('_status', 0)
|
| 410 |
+
r.latency_ms = resp.get('_ms', 0)
|
| 411 |
+
r.raw_response = {k: v for k, v in resp.items() if k not in ('_ms', '_status')}
|
| 412 |
+
|
| 413 |
+
if 'error' in resp:
|
| 414 |
+
if not tc.should_change and tc.input.strip() == "":
|
| 415 |
+
r.verdict = "TN"; r.detail = "Empty input correctly rejected"
|
| 416 |
+
else:
|
| 417 |
+
r.verdict = "ERROR"; r.detail = resp['error']
|
| 418 |
+
return r
|
| 419 |
+
|
| 420 |
+
r.actual_output = resp.get('corrected', '')
|
| 421 |
+
r.suggestions = resp.get('suggestions', [])
|
| 422 |
+
r.changed = r.actual_output != resp.get('original', tc.input)
|
| 423 |
+
|
| 424 |
+
if tc.should_change:
|
| 425 |
+
if r.changed:
|
| 426 |
+
# Check if the right words were corrected
|
| 427 |
+
uncorrected_errors = []
|
| 428 |
+
for ew in tc.error_words:
|
| 429 |
+
if ew in r.actual_output:
|
| 430 |
+
uncorrected_errors.append(ew)
|
| 431 |
+
if uncorrected_errors:
|
| 432 |
+
r.verdict = "FN"
|
| 433 |
+
r.detail = f"Errors NOT fixed: {uncorrected_errors}"
|
| 434 |
+
else:
|
| 435 |
+
r.verdict = "TP"
|
| 436 |
+
r.detail = f"Corrected: {len(r.suggestions)} suggestions"
|
| 437 |
+
else:
|
| 438 |
+
r.verdict = "FN"
|
| 439 |
+
r.detail = f"No changes made. Expected fix for: {tc.error_words}"
|
| 440 |
+
else:
|
| 441 |
+
if r.changed:
|
| 442 |
+
# Check if protected words were corrupted
|
| 443 |
+
corrupted = []
|
| 444 |
+
for cw in tc.correct_words:
|
| 445 |
+
if cw not in r.actual_output and cw in tc.input:
|
| 446 |
+
corrupted.append(cw)
|
| 447 |
+
if corrupted:
|
| 448 |
+
r.verdict = "FP"
|
| 449 |
+
r.detail = f"OVERCORRECTION: corrupted words: {corrupted}"
|
| 450 |
+
elif r.suggestions:
|
| 451 |
+
r.verdict = "FP"
|
| 452 |
+
changes = [f"{s.get('original','')}→{s.get('correction','')}" for s in r.suggestions]
|
| 453 |
+
r.detail = f"Unnecessary changes: {changes}"
|
| 454 |
+
else:
|
| 455 |
+
r.verdict = "TN"
|
| 456 |
+
r.detail = "Text changed but no suggestion objects"
|
| 457 |
+
else:
|
| 458 |
+
r.verdict = "TN"
|
| 459 |
+
r.detail = "Correctly unchanged"
|
| 460 |
+
|
| 461 |
+
return r
|
| 462 |
+
|
| 463 |
+
def run_grammar_test(api: API, tc: TC) -> Result:
|
| 464 |
+
"""Test grammar model via /api/grammar endpoint."""
|
| 465 |
+
r = Result(tc.id, tc.phase, tc.category, tc.input, tc.expected_output)
|
| 466 |
+
resp = api.grammar(tc.input)
|
| 467 |
+
r.api_status = resp.get('_status', 0)
|
| 468 |
+
r.latency_ms = resp.get('_ms', 0)
|
| 469 |
+
r.raw_response = {k: v for k, v in resp.items() if k not in ('_ms', '_status')}
|
| 470 |
+
|
| 471 |
+
if 'error' in resp:
|
| 472 |
+
r.verdict = "ERROR"; r.detail = resp['error']
|
| 473 |
+
return r
|
| 474 |
+
|
| 475 |
+
r.actual_output = resp.get('corrected', resp.get('corrected_text', ''))
|
| 476 |
+
r.changed = r.actual_output != tc.input
|
| 477 |
+
|
| 478 |
+
if tc.should_change:
|
| 479 |
+
if r.changed:
|
| 480 |
+
uncorrected = [ew for ew in tc.error_words if ew in r.actual_output]
|
| 481 |
+
if uncorrected:
|
| 482 |
+
r.verdict = "FN"; r.detail = f"Errors NOT fixed: {uncorrected}"
|
| 483 |
+
else:
|
| 484 |
+
r.verdict = "TP"; r.detail = f"Grammar corrected"
|
| 485 |
+
else:
|
| 486 |
+
r.verdict = "FN"; r.detail = f"No changes made. Expected fix for: {tc.error_words}"
|
| 487 |
+
else:
|
| 488 |
+
if r.changed:
|
| 489 |
+
corrupted = [cw for cw in tc.correct_words if cw not in r.actual_output and cw in tc.input]
|
| 490 |
+
if corrupted:
|
| 491 |
+
r.verdict = "FP"; r.detail = f"OVERCORRECTION: corrupted words: {corrupted}"
|
| 492 |
+
else:
|
| 493 |
+
# Check if it's a stylistic rewrite
|
| 494 |
+
r.verdict = "FP"; r.detail = f"Unnecessary change: '{tc.input[:60]}' → '{r.actual_output[:60]}'"
|
| 495 |
+
else:
|
| 496 |
+
r.verdict = "TN"; r.detail = "Correctly unchanged"
|
| 497 |
+
|
| 498 |
+
return r
|
| 499 |
+
|
| 500 |
+
def run_punctuation_test(api: API, tc: TC) -> Result:
|
| 501 |
+
"""Test punctuation model via /api/punctuation endpoint."""
|
| 502 |
+
r = Result(tc.id, tc.phase, tc.category, tc.input, tc.expected_output)
|
| 503 |
+
resp = api.punctuation(tc.input)
|
| 504 |
+
r.api_status = resp.get('_status', 0)
|
| 505 |
+
r.latency_ms = resp.get('_ms', 0)
|
| 506 |
+
r.raw_response = {k: v for k, v in resp.items() if k not in ('_ms', '_status')}
|
| 507 |
+
|
| 508 |
+
if 'error' in resp:
|
| 509 |
+
r.verdict = "ERROR"; r.detail = resp['error']
|
| 510 |
+
return r
|
| 511 |
+
|
| 512 |
+
r.actual_output = resp.get('corrected', resp.get('corrected_text', ''))
|
| 513 |
+
r.changed = r.actual_output != tc.input
|
| 514 |
+
|
| 515 |
+
# Check if model changed WORDS (not just punctuation)
|
| 516 |
+
punct_chars = set('.,،؛؟!:;?! ')
|
| 517 |
+
orig_words = re.sub(r'[.,،؛؟!:;?!\s]+', ' ', tc.input).strip()
|
| 518 |
+
corr_words = re.sub(r'[.,،؛؟!:;?!\s]+', ' ', r.actual_output).strip()
|
| 519 |
+
word_change = orig_words != corr_words
|
| 520 |
+
|
| 521 |
+
if word_change:
|
| 522 |
+
r.verdict = "FP"
|
| 523 |
+
r.detail = f"WORD CHANGE in punctuation model: '{orig_words[:50]}' → '{corr_words[:50]}'"
|
| 524 |
+
return r
|
| 525 |
+
|
| 526 |
+
if tc.should_change:
|
| 527 |
+
if r.changed:
|
| 528 |
+
r.verdict = "TP"; r.detail = f"Punctuation added"
|
| 529 |
+
else:
|
| 530 |
+
r.verdict = "FN"; r.detail = "No punctuation added"
|
| 531 |
+
else:
|
| 532 |
+
if r.changed:
|
| 533 |
+
r.verdict = "FP"; r.detail = f"Over-punctuated: '{r.actual_output[:80]}'"
|
| 534 |
+
else:
|
| 535 |
+
r.verdict = "TN"; r.detail = "Correctly unchanged"
|
| 536 |
+
|
| 537 |
+
return r
|
| 538 |
+
|
| 539 |
+
def run_pipeline_test(api: API, tc: TC) -> Result:
|
| 540 |
+
"""Test full pipeline via /api/analyze."""
|
| 541 |
+
r = Result(tc.id, tc.phase, tc.category, tc.input, tc.expected_output)
|
| 542 |
+
resp = api.analyze(tc.input)
|
| 543 |
+
r.api_status = resp.get('_status', 0)
|
| 544 |
+
r.latency_ms = resp.get('_ms', 0)
|
| 545 |
+
r.raw_response = {k: v for k, v in resp.items() if k not in ('_ms', '_status')}
|
| 546 |
+
|
| 547 |
+
if 'error' in resp:
|
| 548 |
+
if tc.category in ('edge_empty', 'edge_short', 'edge_english') or tc.input.strip() == "":
|
| 549 |
+
r.verdict = "TN"; r.detail = f"Edge case handled: {resp.get('error','')}"
|
| 550 |
+
else:
|
| 551 |
+
r.verdict = "ERROR"; r.detail = resp['error']
|
| 552 |
+
return r
|
| 553 |
+
|
| 554 |
+
original = resp.get('original', tc.input)
|
| 555 |
+
r.actual_output = resp.get('corrected', '')
|
| 556 |
+
r.suggestions = resp.get('suggestions', [])
|
| 557 |
+
r.changed = r.actual_output != original
|
| 558 |
+
|
| 559 |
+
# ── Span alignment check ──
|
| 560 |
+
span_errors = []
|
| 561 |
+
for s in r.suggestions:
|
| 562 |
+
start, end = s.get('start', 0), s.get('end', 0)
|
| 563 |
+
orig_text = s.get('original', '')
|
| 564 |
+
actual_slice = original[start:end]
|
| 565 |
+
if actual_slice != orig_text and orig_text:
|
| 566 |
+
span_errors.append(f"SPAN[{start}:{end}] expected='{orig_text}' got='{actual_slice}'")
|
| 567 |
+
|
| 568 |
+
if span_errors:
|
| 569 |
+
r.verdict = "FP"
|
| 570 |
+
r.detail = f"SPAN MISMATCH: {'; '.join(span_errors[:3])}"
|
| 571 |
+
return r
|
| 572 |
+
|
| 573 |
+
# ── Apply-all reconstruction check ──
|
| 574 |
+
if tc.category == "apply_all" and r.suggestions:
|
| 575 |
+
rebuilt = original
|
| 576 |
+
for s in sorted(r.suggestions, key=lambda x: -x['start']):
|
| 577 |
+
rebuilt = rebuilt[:s['start']] + s['correction'] + rebuilt[s['end']:]
|
| 578 |
+
if rebuilt != r.actual_output:
|
| 579 |
+
r.verdict = "FP"
|
| 580 |
+
r.detail = f"APPLY-ALL MISMATCH: rebuilt≠corrected"
|
| 581 |
+
return r
|
| 582 |
+
|
| 583 |
+
if tc.should_change:
|
| 584 |
+
if r.changed:
|
| 585 |
+
uncorrected = [ew for ew in tc.error_words if ew in r.actual_output]
|
| 586 |
+
if uncorrected:
|
| 587 |
+
r.verdict = "FN"; r.detail = f"Errors NOT fixed: {uncorrected}"
|
| 588 |
+
else:
|
| 589 |
+
r.verdict = "TP"; r.detail = f"{len(r.suggestions)} fixes applied"
|
| 590 |
+
else:
|
| 591 |
+
r.verdict = "FN"; r.detail = f"No changes made. Expected fix for: {tc.error_words}"
|
| 592 |
+
else:
|
| 593 |
+
if r.changed:
|
| 594 |
+
corrupted = [cw for cw in tc.correct_words if cw not in r.actual_output and cw in tc.input]
|
| 595 |
+
if corrupted:
|
| 596 |
+
r.verdict = "FP"; r.detail = f"OVERCORRECTION: corrupted: {corrupted}"
|
| 597 |
+
elif r.suggestions:
|
| 598 |
+
changes = [f"{s.get('original','')}→{s.get('correction','')}" for s in r.suggestions[:5]]
|
| 599 |
+
r.verdict = "FP"; r.detail = f"Unnecessary changes: {changes}"
|
| 600 |
+
else:
|
| 601 |
+
r.verdict = "TN"; r.detail = "Minor change, no suggestion objects"
|
| 602 |
+
else:
|
| 603 |
+
r.verdict = "TN"; r.detail = "Correctly unchanged"
|
| 604 |
+
|
| 605 |
+
return r
|
| 606 |
+
|
| 607 |
+
def run_adversarial_test(api: API, tc: TC) -> Result:
|
| 608 |
+
"""Run adversarial tests through full pipeline."""
|
| 609 |
+
return run_pipeline_test(api, tc)
|
| 610 |
+
|
| 611 |
+
# ══════════════════════════════════════════════════��════════════════════════════
|
| 612 |
+
# METRICS
|
| 613 |
+
# ═══════════════════════════════════════════════════════════════════════════════
|
| 614 |
+
|
| 615 |
+
def calc_metrics(results: List[Result]) -> dict:
|
| 616 |
+
tp = sum(1 for r in results if r.verdict == "TP")
|
| 617 |
+
fp = sum(1 for r in results if r.verdict == "FP")
|
| 618 |
+
tn = sum(1 for r in results if r.verdict == "TN")
|
| 619 |
+
fn = sum(1 for r in results if r.verdict == "FN")
|
| 620 |
+
err = sum(1 for r in results if r.verdict == "ERROR")
|
| 621 |
+
total = len(results)
|
| 622 |
+
precision = tp / (tp + fp) if (tp + fp) > 0 else 0
|
| 623 |
+
recall = tp / (tp + fn) if (tp + fn) > 0 else 0
|
| 624 |
+
f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0
|
| 625 |
+
fpr = fp / (fp + tn) if (fp + tn) > 0 else 0
|
| 626 |
+
fnr = fn / (fn + tp) if (fn + tp) > 0 else 0
|
| 627 |
+
latencies = [r.latency_ms for r in results if r.latency_ms > 0]
|
| 628 |
+
p50 = sorted(latencies)[len(latencies)//2] if latencies else 0
|
| 629 |
+
p95 = sorted(latencies)[int(len(latencies)*0.95)] if latencies else 0
|
| 630 |
+
p99 = sorted(latencies)[int(len(latencies)*0.99)] if latencies else 0
|
| 631 |
+
|
| 632 |
+
return {
|
| 633 |
+
"total": total, "TP": tp, "FP": fp, "TN": tn, "FN": fn, "ERROR": err,
|
| 634 |
+
"precision": round(precision, 4),
|
| 635 |
+
"recall": round(recall, 4),
|
| 636 |
+
"f1": round(f1, 4),
|
| 637 |
+
"false_positive_rate": round(fpr, 4),
|
| 638 |
+
"false_negative_rate": round(fnr, 4),
|
| 639 |
+
"overcorrection_rate": round(fp / max(1, total), 4),
|
| 640 |
+
"undercorrection_rate": round(fn / max(1, total), 4),
|
| 641 |
+
"latency_p50_ms": p50,
|
| 642 |
+
"latency_p95_ms": p95,
|
| 643 |
+
"latency_p99_ms": p99,
|
| 644 |
+
}
|
| 645 |
+
|
| 646 |
+
# ═══════════════════════════════════════════════════════════════════════════════
|
| 647 |
+
# MAIN
|
| 648 |
+
# ═══════════════════════════════════════════════════════════════════════════════
|
| 649 |
+
|
| 650 |
+
def main():
|
| 651 |
+
parser = argparse.ArgumentParser()
|
| 652 |
+
parser.add_argument("--url", default=DEFAULT_URL)
|
| 653 |
+
parser.add_argument("--phase", nargs="*", default=["ALL"])
|
| 654 |
+
parser.add_argument("--out", default="phase9_results.json")
|
| 655 |
+
args = parser.parse_args()
|
| 656 |
+
|
| 657 |
+
api = API(args.url)
|
| 658 |
+
phases = [p.upper() for p in args.phase]
|
| 659 |
+
run_all = "ALL" in phases
|
| 660 |
+
|
| 661 |
+
print(f"[P9] Target: {args.url}")
|
| 662 |
+
print(f"[P9] Phases: {phases}")
|
| 663 |
+
|
| 664 |
+
all_results = []
|
| 665 |
+
all_metrics = {}
|
| 666 |
+
|
| 667 |
+
# ── Phase A: Spelling ──
|
| 668 |
+
if run_all or "A" in phases:
|
| 669 |
+
tests = build_spelling_tests()
|
| 670 |
+
print(f"\n{'='*60}")
|
| 671 |
+
print(f"PHASE A — RAW SPELLING ({len(tests)} tests)")
|
| 672 |
+
print(f"{'='*60}")
|
| 673 |
+
results = []
|
| 674 |
+
for i, tc in enumerate(tests):
|
| 675 |
+
print(f" [{i+1}/{len(tests)}] {tc.id} {tc.category}: ", end="", flush=True)
|
| 676 |
+
r = run_spelling_test(api, tc)
|
| 677 |
+
results.append(r)
|
| 678 |
+
icon = {"TP":"✅","TN":"✅","FP":"❌","FN":"⚠️","ERROR":"💥"}[r.verdict]
|
| 679 |
+
print(f"{icon} {r.verdict} ({r.latency_ms}ms) {r.detail[:70]}")
|
| 680 |
+
m = calc_metrics(results)
|
| 681 |
+
all_metrics["Phase_A_Spelling"] = m
|
| 682 |
+
all_results.extend(results)
|
| 683 |
+
print(f"\n Precision={m['precision']} Recall={m['recall']} F1={m['f1']}")
|
| 684 |
+
print(f" FPR={m['false_positive_rate']} FNR={m['false_negative_rate']}")
|
| 685 |
+
print(f" Overcorrection={m['overcorrection_rate']} Undercorrection={m['undercorrection_rate']}")
|
| 686 |
+
print(f" Latency p50={m['latency_p50_ms']}ms p95={m['latency_p95_ms']}ms p99={m['latency_p99_ms']}ms")
|
| 687 |
+
|
| 688 |
+
# ── Phase B: Grammar ──
|
| 689 |
+
if run_all or "B" in phases:
|
| 690 |
+
tests = build_grammar_tests()
|
| 691 |
+
print(f"\n{'='*60}")
|
| 692 |
+
print(f"PHASE B — RAW GRAMMAR ({len(tests)} tests)")
|
| 693 |
+
print(f"{'='*60}")
|
| 694 |
+
results = []
|
| 695 |
+
for i, tc in enumerate(tests):
|
| 696 |
+
print(f" [{i+1}/{len(tests)}] {tc.id} {tc.category}: ", end="", flush=True)
|
| 697 |
+
r = run_grammar_test(api, tc)
|
| 698 |
+
results.append(r)
|
| 699 |
+
icon = {"TP":"✅","TN":"✅","FP":"❌","FN":"⚠️","ERROR":"💥"}[r.verdict]
|
| 700 |
+
print(f"{icon} {r.verdict} ({r.latency_ms}ms) {r.detail[:70]}")
|
| 701 |
+
m = calc_metrics(results)
|
| 702 |
+
all_metrics["Phase_B_Grammar"] = m
|
| 703 |
+
all_results.extend(results)
|
| 704 |
+
print(f"\n Precision={m['precision']} Recall={m['recall']} F1={m['f1']}")
|
| 705 |
+
print(f" FPR={m['false_positive_rate']} FNR={m['false_negative_rate']}")
|
| 706 |
+
|
| 707 |
+
# ── Phase C: Punctuation ──
|
| 708 |
+
if run_all or "C" in phases:
|
| 709 |
+
tests = build_punctuation_tests()
|
| 710 |
+
print(f"\n{'='*60}")
|
| 711 |
+
print(f"PHASE C — RAW PUNCTUATION ({len(tests)} tests)")
|
| 712 |
+
print(f"{'='*60}")
|
| 713 |
+
results = []
|
| 714 |
+
for i, tc in enumerate(tests):
|
| 715 |
+
print(f" [{i+1}/{len(tests)}] {tc.id} {tc.category}: ", end="", flush=True)
|
| 716 |
+
r = run_punctuation_test(api, tc)
|
| 717 |
+
results.append(r)
|
| 718 |
+
icon = {"TP":"✅","TN":"✅","FP":"❌","FN":"⚠️","ERROR":"💥"}[r.verdict]
|
| 719 |
+
print(f"{icon} {r.verdict} ({r.latency_ms}ms) {r.detail[:70]}")
|
| 720 |
+
m = calc_metrics(results)
|
| 721 |
+
all_metrics["Phase_C_Punctuation"] = m
|
| 722 |
+
all_results.extend(results)
|
| 723 |
+
print(f"\n Precision={m['precision']} Recall={m['recall']} F1={m['f1']}")
|
| 724 |
+
|
| 725 |
+
# ── Phase D: Full Pipeline ──
|
| 726 |
+
if run_all or "D" in phases:
|
| 727 |
+
tests = build_pipeline_tests()
|
| 728 |
+
print(f"\n{'='*60}")
|
| 729 |
+
print(f"PHASE D — FULL PIPELINE ({len(tests)} tests)")
|
| 730 |
+
print(f"{'='*60}")
|
| 731 |
+
results = []
|
| 732 |
+
for i, tc in enumerate(tests):
|
| 733 |
+
print(f" [{i+1}/{len(tests)}] {tc.id} {tc.category}: ", end="", flush=True)
|
| 734 |
+
r = run_pipeline_test(api, tc)
|
| 735 |
+
results.append(r)
|
| 736 |
+
icon = {"TP":"✅","TN":"✅","FP":"❌","FN":"⚠️","ERROR":"💥"}[r.verdict]
|
| 737 |
+
print(f"{icon} {r.verdict} ({r.latency_ms}ms) {r.detail[:70]}")
|
| 738 |
+
m = calc_metrics(results)
|
| 739 |
+
all_metrics["Phase_D_Pipeline"] = m
|
| 740 |
+
all_results.extend(results)
|
| 741 |
+
print(f"\n Precision={m['precision']} Recall={m['recall']} F1={m['f1']}")
|
| 742 |
+
print(f" Span errors: {sum(1 for r in results if 'SPAN' in r.detail)}")
|
| 743 |
+
print(f" Apply-all errors: {sum(1 for r in results if 'APPLY-ALL' in r.detail)}")
|
| 744 |
+
|
| 745 |
+
# ── Phase E: Adversarial ──
|
| 746 |
+
if run_all or "E" in phases:
|
| 747 |
+
tests = build_adversarial_tests()
|
| 748 |
+
print(f"\n{'='*60}")
|
| 749 |
+
print(f"PHASE E — ADVERSARIAL ({len(tests)} tests)")
|
| 750 |
+
print(f"{'='*60}")
|
| 751 |
+
results = []
|
| 752 |
+
for i, tc in enumerate(tests):
|
| 753 |
+
print(f" [{i+1}/{len(tests)}] {tc.id} {tc.category}: ", end="", flush=True)
|
| 754 |
+
r = run_adversarial_test(api, tc)
|
| 755 |
+
results.append(r)
|
| 756 |
+
icon = {"TP":"✅","TN":"✅","FP":"❌","FN":"⚠️","ERROR":"💥"}[r.verdict]
|
| 757 |
+
print(f"{icon} {r.verdict} ({r.latency_ms}ms) {r.detail[:70]}")
|
| 758 |
+
m = calc_metrics(results)
|
| 759 |
+
all_metrics["Phase_E_Adversarial"] = m
|
| 760 |
+
all_results.extend(results)
|
| 761 |
+
|
| 762 |
+
# ── Summary ──
|
| 763 |
+
print(f"\n{'='*60}")
|
| 764 |
+
print(f"FINAL SUMMARY")
|
| 765 |
+
print(f"{'='*60}")
|
| 766 |
+
total_tp = sum(1 for r in all_results if r.verdict == "TP")
|
| 767 |
+
total_fp = sum(1 for r in all_results if r.verdict == "FP")
|
| 768 |
+
total_tn = sum(1 for r in all_results if r.verdict == "TN")
|
| 769 |
+
total_fn = sum(1 for r in all_results if r.verdict == "FN")
|
| 770 |
+
total_err = sum(1 for r in all_results if r.verdict == "ERROR")
|
| 771 |
+
print(f" Total tests: {len(all_results)}")
|
| 772 |
+
print(f" TP (correct fix): {total_tp}")
|
| 773 |
+
print(f" TN (correct no-change): {total_tn}")
|
| 774 |
+
print(f" FP (overcorrection): {total_fp}")
|
| 775 |
+
print(f" FN (undercorrection): {total_fn}")
|
| 776 |
+
print(f" ERROR: {total_err}")
|
| 777 |
+
print(f"\n PASS rate: {(total_tp+total_tn)/max(1,len(all_results))*100:.1f}%")
|
| 778 |
+
print(f" FAIL rate: {(total_fp+total_fn)/max(1,len(all_results))*100:.1f}%")
|
| 779 |
+
|
| 780 |
+
# Critical failures
|
| 781 |
+
fps = [r for r in all_results if r.verdict == "FP"]
|
| 782 |
+
if fps:
|
| 783 |
+
print(f"\n🚨 FALSE POSITIVES ({len(fps)}):")
|
| 784 |
+
for r in fps[:20]:
|
| 785 |
+
print(f" {r.tc_id} [{r.category}] {r.detail[:90]}")
|
| 786 |
+
|
| 787 |
+
fns = [r for r in all_results if r.verdict == "FN"]
|
| 788 |
+
if fns:
|
| 789 |
+
print(f"\n⚠️ FALSE NEGATIVES ({len(fns)}):")
|
| 790 |
+
for r in fns[:20]:
|
| 791 |
+
print(f" {r.tc_id} [{r.category}] {r.detail[:90]}")
|
| 792 |
+
|
| 793 |
+
# Save
|
| 794 |
+
output = {
|
| 795 |
+
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
| 796 |
+
"target": args.url,
|
| 797 |
+
"metrics": all_metrics,
|
| 798 |
+
"total_tests": len(all_results),
|
| 799 |
+
"summary": {
|
| 800 |
+
"TP": total_tp, "TN": total_tn, "FP": total_fp, "FN": total_fn, "ERROR": total_err,
|
| 801 |
+
"pass_rate": round((total_tp+total_tn)/max(1,len(all_results)), 4),
|
| 802 |
+
},
|
| 803 |
+
"results": [asdict(r) for r in all_results],
|
| 804 |
+
}
|
| 805 |
+
with open(args.out, 'w', encoding='utf-8') as f:
|
| 806 |
+
json.dump(output, f, ensure_ascii=False, indent=2)
|
| 807 |
+
print(f"\n[P9] Results saved to {args.out}")
|
| 808 |
+
|
| 809 |
+
|
| 810 |
+
if __name__ == "__main__":
|
| 811 |
+
main()
|