Commit ·
9786f39
1
Parent(s): 1519f71
Fix punctuation alignment and hallucinations, add punctuation safety rules, add kana/inna grammar rules
Browse files- BAYAN_COMPLETE_AUDIT.md +313 -62
- extension/shared/supabase.min.js +0 -0
- requirements.txt +1 -0
- src/app.py +3 -1
- src/nlp/grammar/grammar_rules.py +21 -30
- src/nlp/punctuation/punctuation_rules.py +64 -41
- src/nlp/punctuation/punctuation_service.py +24 -1
BAYAN_COMPLETE_AUDIT.md
CHANGED
|
@@ -1,115 +1,366 @@
|
|
| 1 |
# BAYAN — Complete Product, Codebase & Extension Deep Audit
|
| 2 |
|
| 3 |
-
|
|
|
|
| 4 |
|
| 5 |
-
|
| 6 |
-
The core architecture is built around a centralized Flask API hosted on Hugging Face Spaces (`bayan10-bayan-api.hf.space`), which serves NLP models for spelling, grammar, punctuation, summarization, dialect conversion, and Quranic validation.
|
| 7 |
|
| 8 |
-
|
| 9 |
-
- **Frontend:** Vanilla JS, HTML, CSS (Tailwind via CDN). Uses a custom `contenteditable` editor engine.
|
| 10 |
-
- **Backend:** Flask API handling model inference.
|
| 11 |
-
- **Database/Auth:** Supabase for cloud persistence, document sync, and authentication (Guest & Google).
|
| 12 |
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
---
|
| 19 |
|
| 20 |
## 2. Feature Inventory
|
| 21 |
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
|
| 25 |
-
|
|
| 26 |
-
| **
|
| 27 |
-
| **
|
| 28 |
-
| **
|
| 29 |
-
| **
|
| 30 |
-
| **
|
| 31 |
-
| **
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
---
|
| 34 |
|
| 35 |
## 3. Website vs Extension Comparison
|
| 36 |
|
| 37 |
-
### Authentication
|
| 38 |
-
- **Website:** Supports Guest login and Google login via Supabase. State is maintained via `auth.js`.
|
| 39 |
-
- **Extension:** Uses isolated `chrome.storage.local/session`. **Does not share sessions** with the website.
|
| 40 |
-
- **Priority:** **High**. Users cannot access their cloud documents or synced preferences from the extension.
|
| 41 |
|
| 42 |
-
|
| 43 |
-
-
|
| 44 |
-
|
| 45 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
### Documents
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
---
|
| 53 |
|
| 54 |
-
## 4. Missing Features
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
|
| 60 |
---
|
| 61 |
|
| 62 |
-
## 5. Bugs
|
| 63 |
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
|
| 68 |
---
|
| 69 |
|
| 70 |
## 6. Security Issues
|
| 71 |
|
| 72 |
-
|
| 73 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
|
| 75 |
---
|
| 76 |
|
| 77 |
## 7. Performance Issues
|
| 78 |
|
| 79 |
-
|
| 80 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
|
| 82 |
---
|
| 83 |
|
| 84 |
## 8. UX Problems
|
| 85 |
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
|
| 90 |
---
|
| 91 |
|
| 92 |
## 9. Technical Debt
|
| 93 |
|
| 94 |
-
|
| 95 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
|
| 97 |
---
|
| 98 |
|
| 99 |
-
##
|
| 100 |
|
| 101 |
-
|
| 102 |
-
- **Goal:** Share Supabase session between Website and Extension.
|
| 103 |
-
- **Action:** Implement `chrome.cookies` or message passing to inject the Supabase JWT into the extension.
|
| 104 |
|
| 105 |
-
|
| 106 |
-
-
|
| 107 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
|
|
|
|
|
|
| 112 |
|
| 113 |
-
|
| 114 |
-
- **Goal:** Reduce tech debt.
|
| 115 |
-
- **Action:** Refactor `shared/` folder to act as a true submodule for both web and extension. Lazy load heavy vendor scripts on the web.
|
|
|
|
| 1 |
# BAYAN — Complete Product, Codebase & Extension Deep Audit
|
| 2 |
|
| 3 |
+
> **Audit Date:** 2026-06-26
|
| 4 |
+
> **Auditor Perspective:** Product Manager + Senior Frontend + Backend Architect + Extension Engineer + SaaS Reviewer
|
| 5 |
|
| 6 |
+
---
|
|
|
|
| 7 |
|
| 8 |
+
## 1. Current System Overview
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
+
### Architecture Map
|
| 11 |
+
|
| 12 |
+
```
|
| 13 |
+
┌──────────────────────────────────────────────────────┐
|
| 14 |
+
│ BAYAN ECOSYSTEM │
|
| 15 |
+
│ │
|
| 16 |
+
│ ┌─────────┐ ┌──────────┐ ┌─────────────────┐ │
|
| 17 |
+
│ │ Website │───▶│ Flask API │───▶│ NLP Pipeline │ │
|
| 18 |
+
│ │ (SPA) │ │ (app.py) │ │ Spell/Gram/Punct│ │
|
| 19 |
+
│ └─────────┘ └──────────┘ └─────────────────┘ │
|
| 20 |
+
│ │ │ │ │
|
| 21 |
+
│ │ │ ┌─────────────────┐ │
|
| 22 |
+
│ │ ├─────────▶│ HF Models │ │
|
| 23 |
+
│ │ │ │ Summarization │ │
|
| 24 |
+
│ │ │ │ Grammar (Gradio)│ │
|
| 25 |
+
│ │ │ └─────────────────┘ │
|
| 26 |
+
│ │ │ │
|
| 27 |
+
│ ┌─────────┐ ┌──────────┐ ┌─────────────────┐ │
|
| 28 |
+
│ │Supabase │◀───│ Auth │───▶│ Documents DB │ │
|
| 29 |
+
│ │ (Cloud) │ │ Module │ │ Settings Sync │ │
|
| 30 |
+
│ └─────────┘ └──────────┘ └─────────────────┘ │
|
| 31 |
+
│ │
|
| 32 |
+
│ ┌────────────────────────────────────────────────┐ │
|
| 33 |
+
│ │ Chrome Extension (MV3) │ │
|
| 34 |
+
│ │ ┌──────────┐ ┌──────────┐ ┌───────────────┐ │ │
|
| 35 |
+
│ │ │ Content │ │Background│ │ Side Panel │ │ │
|
| 36 |
+
│ │ │ Script │ │ Worker │ │ + Popup │ │ │
|
| 37 |
+
│ │ └──────────┘ └──────────┘ └───────────────┘ │ │
|
| 38 |
+
│ └────────────────────────────────────────────────┘ │
|
| 39 |
+
└──────────────────────────────────────────────────────┘
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
### Technology Stack
|
| 43 |
+
|
| 44 |
+
| Layer | Technology | Notes |
|
| 45 |
+
|-------|-----------|-------|
|
| 46 |
+
| **Frontend** | Vanilla JS, HTML, CSS (Tailwind CDN) | Custom `contenteditable` editor engine |
|
| 47 |
+
| **Backend** | Flask (Python) | Single monolith `app.py` — 2,844 lines |
|
| 48 |
+
| **NLP Pipeline** | Custom Python modules | Spelling, Grammar, Punctuation, Autocomplete, Dialect |
|
| 49 |
+
| **AI Models** | Transformer-based | Summarization (local), Grammar (Gradio proxy), Spelling (CAMeL + custom) |
|
| 50 |
+
| **Database** | Supabase (PostgreSQL) | Documents, profiles, user settings |
|
| 51 |
+
| **Auth** | Supabase Auth | Guest (anonymous), Google OAuth |
|
| 52 |
+
| **Deployment** | HuggingFace Spaces (Docker) | CPU-only free tier |
|
| 53 |
+
| **Extension** | Chrome MV3 | Background SW, Content Script, Side Panel, Popup |
|
| 54 |
+
|
| 55 |
+
### File Structure Summary
|
| 56 |
+
|
| 57 |
+
| Directory | Files | Purpose |
|
| 58 |
+
|-----------|-------|---------|
|
| 59 |
+
| `src/` | 6 core files | Backend + HTML + CSS |
|
| 60 |
+
| `src/js/` | 8 JS files + 7 subdirs | Frontend logic |
|
| 61 |
+
| `src/js/auth/` | 5 files | Supabase auth (client, session, UI) |
|
| 62 |
+
| `src/js/documents/` | 4 files | Local doc management + export |
|
| 63 |
+
| `src/js/documents-cloud/` | 3 files | Supabase CRUD for documents |
|
| 64 |
+
| `src/js/sync/` | 3 files | Offline queue + conflict resolution |
|
| 65 |
+
| `src/js/settings-sync/` | 2 files | User settings cloud persistence |
|
| 66 |
+
| `src/nlp/` | 6 subdirs | All NLP processing modules |
|
| 67 |
+
| `extension/` | 8 files + 4 subdirs | Chrome Extension |
|
| 68 |
+
| `extension/shared/` | 9 files | Shared utilities (api, renderer, patches) |
|
| 69 |
+
| `extension/sidepanel/` | 3 files | Side panel UI |
|
| 70 |
+
| `tests/` | 16 test files | Backend unit tests |
|
| 71 |
+
| `extension/tests/` | 8 files | Extension integration tests |
|
| 72 |
|
| 73 |
---
|
| 74 |
|
| 75 |
## 2. Feature Inventory
|
| 76 |
|
| 77 |
+
### Core AI Features
|
| 78 |
+
|
| 79 |
+
| Feature | Backend API | Website Frontend | Extension | Files |
|
| 80 |
+
|---------|------------|-----------------|-----------|-------|
|
| 81 |
+
| **Spelling Correction** | ✅ `/api/spelling` + `/api/analyze` | ✅ Full (highlights, suggestions, apply) | ✅ Inline overlay + Popup + SidePanel | `nlp/spelling/`, `editor.js`, `renderer.js` |
|
| 82 |
+
| **Grammar Correction** | ✅ `/api/grammar` + `/api/analyze` | ✅ Full (via Gradio proxy to HF model) | ✅ Inline overlay + Popup + SidePanel | `nlp/grammar/`, `hf_inference.py` |
|
| 83 |
+
| **Punctuation** | ✅ `/api/punctuation` + `/api/analyze` | ✅ Full (PuncAra-v1 model) | ✅ Inline overlay + Popup + SidePanel | `nlp/punctuation/` |
|
| 84 |
+
| **Summarization** | ✅ `/api/summarize` | ✅ Full (tab in editor, length control) | ✅ Popup tab + SidePanel tab | `model_loader.py`, `summaries-api.js` |
|
| 85 |
+
| **AutoComplete** | ✅ `/api/autocomplete` | ✅ Ghost text + dropdown in editor | ⚠️ SidePanel text-box only, NO inline ghost text | `autocomplete.js`, sidepanel `btnAutocomplete` |
|
| 86 |
+
| **Dialect→MSA** | ✅ `/api/dialect` | ✅ Dedicated editor tab | ✅ SidePanel tab (basic text→text) | `nlp/dialect/` |
|
| 87 |
+
| **Quran Verification** | ✅ `/api/quran` | ✅ Dedicated editor tab | ✅ SidePanel tab (basic text→text) | `quran.py`, `quran_master.db` |
|
| 88 |
+
|
| 89 |
+
### Platform Features
|
| 90 |
+
|
| 91 |
+
| Feature | Website | Extension (Popup) | Extension (SidePanel) | Extension (Content Script) |
|
| 92 |
+
|---------|---------|-------------------|----------------------|--------------------------|
|
| 93 |
+
| **Authentication** | ✅ Guest + Google | ❌ None | ⚠️ Partial (`initExtensionAuth()` exists but requires web page auth sync) | ⚠️ Listens for `BAYAN_AUTH_SYNC` message from web |
|
| 94 |
+
| **Document Save** | ✅ Supabase CRUD | ❌ None | ⚠️ UI exists (`btnNewDocument`, `btnSaveSelection`) but depends on auth | ❌ None |
|
| 95 |
+
| **Document Load/History** | ✅ Full panel | ❌ None | ⚠️ UI exists (`documentsList`, `historyList`) but depends on auth | ❌ None |
|
| 96 |
+
| **Export (PDF/DOCX/TXT)** | ✅ Full (mammoth.js, docx.js) | ❌ None | ❌ None | ❌ None |
|
| 97 |
+
| **Import (TXT/DOCX)** | ✅ Full | ❌ None | ❌ None | ❌ None |
|
| 98 |
+
| **Settings Sync** | ✅ Supabase | ❌ None | ⚠️ Placeholder (`syncExtensionSettings()`) | ❌ None |
|
| 99 |
+
| **Theme Toggle** | ✅ Full dark/light | ❌ Hardcoded dark | ✅ Dark only | N/A |
|
| 100 |
+
| **Focus Mode** | ✅ Full | N/A | ❌ None | N/A |
|
| 101 |
+
| **Score Ring** | ✅ Animated SVG | ✅ Simplified | ✅ Simplified | ❌ None |
|
| 102 |
+
| **Writing Score History** | ✅ Sparkline chart | ❌ None | ❌ None | ❌ None |
|
| 103 |
+
| **Error Donut Chart** | ✅ SVG donut | ❌ None | ❌ None | ❌ None |
|
| 104 |
+
| **Offline Mode** | ✅ Graceful degradation | ❌ No offline handling | ❌ No offline handling | ❌ No offline handling |
|
| 105 |
+
| **Keyboard Shortcuts** | ✅ Extensive (Alt+1-3, Ctrl+S, etc.) | ❌ None | ❌ None | ❌ None |
|
| 106 |
|
| 107 |
---
|
| 108 |
|
| 109 |
## 3. Website vs Extension Comparison
|
| 110 |
|
| 111 |
+
### Authentication Flow
|
|
|
|
|
|
|
|
|
|
| 112 |
|
| 113 |
+
| Aspect | Website | Extension | Gap |
|
| 114 |
+
|--------|---------|-----------|-----|
|
| 115 |
+
| Guest login | ✅ `signInAnonymously()` | ❌ | **Critical** — extension users can't persist anything |
|
| 116 |
+
| Google OAuth | ✅ `signInWithOAuth()` | ❌ | **High** |
|
| 117 |
+
| Session restore | ✅ `restoreSession()` via Supabase | ❌ | **High** |
|
| 118 |
+
| Auth state sync | ✅ `onAuthStateChange()` | ⚠️ Listens for `BAYAN_AUTH_SYNC` postMessage but only works when user visits Bayan website with extension installed | **High** — unreliable |
|
| 119 |
+
| Auth-gated features | ✅ Documents, sync, settings | ⚠️ UI elements exist but non-functional without auth | **High** |
|
| 120 |
+
|
| 121 |
+
### AI Feature Comparison
|
| 122 |
+
|
| 123 |
+
| Feature | Website UX | Extension UX | Parity? |
|
| 124 |
+
|---------|-----------|-------------|---------|
|
| 125 |
+
| Analyze (S+G+P) | Rich editor with inline highlights, suggestion sidebar, popover tooltip, apply/dismiss per-suggestion | **Content Script:** Overlay marks + tooltip. **Popup/SidePanel:** Textarea + suggestion cards | ⚠️ Functional but UX gap |
|
| 126 |
+
| Summarize | Editor tab with radio buttons (short/medium/long) | Popup/SidePanel textarea with radio buttons | ✅ Near parity |
|
| 127 |
+
| AutoComplete | **Ghost text** inside editor (Tab to accept) | SidePanel has a text box with "إكمال" button but NO inline ghost text on 3rd party sites | **Medium** — missing the core UX |
|
| 128 |
+
| Dialect | Dedicated editor tab with "Convert" button | SidePanel tab with text box and "Convert" button | ✅ Near parity |
|
| 129 |
+
| Quran | Dedicated editor tab with search | SidePanel tab with text box and search | ✅ Near parity |
|
| 130 |
|
| 131 |
### Documents
|
| 132 |
+
|
| 133 |
+
| Aspect | Website | Extension | Gap |
|
| 134 |
+
|--------|---------|-----------|-----|
|
| 135 |
+
| Create document | ✅ `createDocument()` | ⚠️ Button exists in SidePanel but blocked by no auth | **High** |
|
| 136 |
+
| List documents | ✅ Desktop sidebar panel | ⚠️ `documentsList` in SidePanel workspace tab, blocked by no auth | **High** |
|
| 137 |
+
| Save/auto-save | ✅ Debounced sync via `SyncManager` | ❌ | **High** |
|
| 138 |
+
| Export PDF/DOCX | ✅ `export.js` | ❌ | **Medium** |
|
| 139 |
+
| Import | ✅ `import.js` (TXT, DOCX) | ❌ | **Low** |
|
| 140 |
|
| 141 |
---
|
| 142 |
|
| 143 |
+
## 4. Missing Features
|
| 144 |
+
|
| 145 |
+
### Critical (Blocks Production)
|
| 146 |
+
|
| 147 |
+
| # | Issue | Impact | Solution |
|
| 148 |
+
|---|-------|--------|----------|
|
| 149 |
+
| C1 | **`.env` file committed to Git** | Supabase URL and anon key are in the repo. While anon key is safe for client use, this is a security anti-pattern and may expose the project URL. | Remove `.env` from Git history, use HF Spaces secrets exclusively. `.gitignore` has `.env` but it was committed before the rule was added. |
|
| 150 |
+
| C2 | **CORS wildcard `origins: "*"`** | Any website can call `/api/analyze`, `/api/summarize`, etc. directly. Abusers can drain compute. | Restrict CORS to `bayan10-bayan-api.hf.space` + extension origin `chrome-extension://<id>`. |
|
| 151 |
+
| C3 | **No rate limiting on API** | No throttle on any endpoint. A single user can overwhelm the free-tier HF Space. | Add Flask-Limiter or simple in-memory token bucket. |
|
| 152 |
+
|
| 153 |
+
### High (Important Feature Gap)
|
| 154 |
+
|
| 155 |
+
| # | Issue | Impact | Solution |
|
| 156 |
+
|---|-------|--------|----------|
|
| 157 |
+
| H1 | Extension has no auth | Users cannot access cloud docs, settings, or history from extension | Implement Supabase auth in extension via `chrome.identity` or shared session from Bayan website |
|
| 158 |
+
| H2 | Extension content script lacks AutoComplete ghost text | The flagship "ghost text" feature doesn't work on 3rd-party sites | Port `autocomplete.js` logic into `content-inline.js` with `/api/autocomplete` calls |
|
| 159 |
+
| H3 | Extension popup/sidepanel have no export | Users cannot export corrected text as PDF/DOCX | Add "Copy as formatted text" or lightweight export |
|
| 160 |
+
| H4 | No `documents` table migration | `supabase/migrations/001_profiles.sql` exists but no migration creates the `documents` table that `documents-api.js` uses | Create `002_documents.sql` migration |
|
| 161 |
+
| H5 | Backend monolith: `app.py` is 2,844 lines | Extremely difficult to maintain, test, or extend | Split into `routes/`, `services/`, `middleware/` modules |
|
| 162 |
+
|
| 163 |
+
### Medium (Improvement Needed)
|
| 164 |
|
| 165 |
+
| # | Issue | Impact | Solution |
|
| 166 |
+
|---|-------|--------|----------|
|
| 167 |
+
| M1 | `src/js/api.js` uses ES module `export` syntax but is loaded via `<script>` tag (not `type="module"`) | The `api.js` exports are **never importable** — the website uses inline `fetch()` calls instead | Either convert to `type="module"` or remove the dead `export` statements |
|
| 168 |
+
| M2 | Extension content script overlay doesn't handle `<iframe>` editors | Rich text editors in iframes (e.g., WordPress Gutenberg, TinyMCE) are invisible to the content script | Use `all_frames: true` in manifest or detect iframe editors |
|
| 169 |
+
| M3 | Duplicated suggestion rendering logic | `ui.js` (website) and `bayan-ui.js` (extension) implement the same card HTML generation | Extract to shared package |
|
| 170 |
+
| M4 | Extension `popup.js` (498 lines) and `sidepanel.js` (702 lines) share ~60% identical code | Maintenance nightmare — fixing a bug requires changes in 2+ files | Refactor into shared modules with UI-specific wrappers |
|
| 171 |
+
| M5 | Grammar model uses Gradio proxy with SSE streaming | Creates a hard dependency on external `mohammedahmedezz2004-bayan-arabic-grammarly-correction.hf.space`. If that Space goes down, grammar breaks. | Host the grammar model directly on the Bayan Space, or add fallback |
|
| 172 |
+
| M6 | No i18n framework on website | All strings are hardcoded in Arabic HTML. Adding English support requires rewriting HTML | Add simple i18n JSON loader (extension already has `_locales/ar/`) |
|
| 173 |
+
|
| 174 |
+
### Low (Nice to Have)
|
| 175 |
+
|
| 176 |
+
| # | Issue | Impact | Solution |
|
| 177 |
+
|---|-------|--------|----------|
|
| 178 |
+
| L1 | Extension only has Arabic locale | Cannot be published on Chrome Web Store for non-Arabic users | Add `_locales/en/messages.json` |
|
| 179 |
+
| L2 | No analytics or telemetry | No visibility into usage patterns, error rates, or feature adoption | Add lightweight event tracking (privacy-respecting) |
|
| 180 |
+
| L3 | Heavy vendor libraries loaded synchronously | `mammoth.browser.min.js`, `docx.umd.js`, `html2canvas.min.js` block initial render | Lazy-load on first export action |
|
| 181 |
+
| L4 | No service worker for website | No offline caching for the web app | Add basic SW for static assets |
|
| 182 |
|
| 183 |
---
|
| 184 |
|
| 185 |
+
## 5. Bugs Found
|
| 186 |
|
| 187 |
+
| # | Bug | Severity | Location | Status |
|
| 188 |
+
|---|-----|----------|----------|--------|
|
| 189 |
+
| B1 | `ENABLE_AUTOCOMPLETE_MODEL = False` in `app.py:62` | Medium | `app.py` line 62 | AutoComplete model disabled by default — `/api/autocomplete` still works via lazy-loading, but the flag is misleading |
|
| 190 |
+
| B2 | `src/js/api.js` uses `export` keyword but is not loaded as ES module | Low | `api.js` | Dead code — never actually imported anywhere |
|
| 191 |
+
| B3 | Extension `bayan-api.js` missing functions `bayanAutocomplete`, `bayanDialect`, `bayanQuran` | High | `bayan-api.js` only defines `bayanAnalyze`, `bayanSummarize`, `bayanHealthCheck` | SidePanel calls these undefined functions — will throw `ReferenceError` |
|
| 192 |
+
| B4 | Extension content script overlay position breaks on page scroll (absolute vs fixed positioning) | Medium | `content-inline.js:191` | Overlay uses `window.scrollY` but doesn't update on window resize |
|
| 193 |
+
| B5 | Score sparkline renders with only 2 data points creating a meaningless line | Low | `format.js` | ✅ Fixed (raised minimum to 3 points) |
|
| 194 |
+
| B6 | `dismissAllFiltered()` only removed DOM elements without updating `window.currentSuggestions` | Medium | `format.js` | ✅ Fixed |
|
| 195 |
|
| 196 |
---
|
| 197 |
|
| 198 |
## 6. Security Issues
|
| 199 |
|
| 200 |
+
| # | Issue | Severity | Details |
|
| 201 |
+
|---|-------|----------|---------|
|
| 202 |
+
| S1 | **`.env` committed to repo** | **Critical** | Supabase URL + anon key visible in Git history. While anon keys are designed for client-side use, the URL+key combo allows anyone to make Supabase API calls. |
|
| 203 |
+
| S2 | **CORS `origins: "*"`** | **Critical** | `app.py:94` — allows any origin to call all API endpoints. Enables: (a) compute theft, (b) DDoS via free proxy, (c) third-party scraping. |
|
| 204 |
+
| S3 | **No API authentication** | **High** | No JWT, API key, or session check on any endpoint. Extension uses only `host_permissions` scoping. |
|
| 205 |
+
| S4 | **XSS risk in editor** | **Medium** | `setEditorHTML()` injects HTML directly into contenteditable. While `renderer.js` escapes text, any upstream bug in suggestion rendering could inject arbitrary HTML. |
|
| 206 |
+
| S5 | **Supabase RLS incomplete** | **Medium** | Only `profiles` has RLS policies. The `documents` table (if exists) needs RLS to prevent cross-user data access. |
|
| 207 |
+
| S6 | **Extension Trusted Types partial** | **Low** | `content-inline.js` implements `trustedTypes.createPolicy()` with identity transform (`input => input`), which passes the CSP check but provides no actual sanitization. |
|
| 208 |
+
| S7 | **Debug endpoint exposed** | **Low** | `/api/debug-models` is accessible in production and leaks internal model status, memory usage, and startup errors. |
|
| 209 |
|
| 210 |
---
|
| 211 |
|
| 212 |
## 7. Performance Issues
|
| 213 |
|
| 214 |
+
| # | Issue | Severity | Details |
|
| 215 |
+
|---|-------|----------|---------|
|
| 216 |
+
| P1 | **`app.py` is 2,844 lines** | High | Single-file monolith. Every request loads all imports. Cold start on HF Spaces free tier takes ~60s. |
|
| 217 |
+
| P2 | **Vendor JS loaded synchronously** | Medium | `mammoth.browser.min.js` (340KB), `docx.umd.js` (1.2MB), `html2canvas.min.js` (210KB) all load on page start even if never used. |
|
| 218 |
+
| P3 | **Extension content script injected on ALL sites** | Medium | `matches: ["https://*/*", "http://*/*"]` — runs on every page. The `BayanController` module loads even on sites where user never types Arabic. |
|
| 219 |
+
| P4 | **No API response caching on website** | Medium | Every keystroke after debounce triggers a full `/api/analyze` call. Extension has background worker caching, but website doesn't. |
|
| 220 |
+
| P5 | **Grammar Gradio SSE dependency** | Medium | Grammar correction requires streaming from external HF Space. Average latency: 3-8 seconds. Adds significant delay to the analysis pipeline. |
|
| 221 |
+
| P6 | **Quran DB is 23MB** | Low | `quran_master.db` (SQLite, 23MB) is loaded into the Docker container. Fine for now, but limits scaling. |
|
| 222 |
+
| P7 | **No CSS/JS minification** | Low | All assets served unminified. `components.css` alone is 4,125+ lines (~90KB). |
|
| 223 |
|
| 224 |
---
|
| 225 |
|
| 226 |
## 8. UX Problems
|
| 227 |
|
| 228 |
+
| # | Issue | Severity | Details |
|
| 229 |
+
|---|-------|----------|---------|
|
| 230 |
+
| U1 | **Extension content script tooltip clips at viewport edge** | Medium | Tooltip for highlighted errors can overflow off-screen on narrow viewports. No boundary detection. |
|
| 231 |
+
| U2 | **No loading skeleton on website** | Medium | Editor page shows blank white space during model initialization. No skeleton/shimmer to indicate loading. |
|
| 232 |
+
| U3 | **Extension popup has no dialect/quran/autocomplete** | Medium | Only "تصحيح" and "تلخيص" tabs. SidePanel has all features, but popup is the first surface users see. |
|
| 233 |
+
| U4 | **Inconsistent branding between popup and sidepanel** | Low | Popup uses `.bayan-*` class prefix, SidePanel uses `.sp-*` prefix. Different color palettes. |
|
| 234 |
+
| U5 | **No onboarding flow** | Low | First-time users see an empty editor with no guidance. No tooltips, walkthrough, or sample text. |
|
| 235 |
+
| U6 | **Mobile responsiveness incomplete** | Low | Website has responsive breakpoints but bottom-sheet for suggestions lacks smooth gestures. |
|
| 236 |
|
| 237 |
---
|
| 238 |
|
| 239 |
## 9. Technical Debt
|
| 240 |
|
| 241 |
+
### Backend
|
| 242 |
+
|
| 243 |
+
| Item | Severity | Details |
|
| 244 |
+
|------|----------|---------|
|
| 245 |
+
| **Monolith `app.py`** | High | 2,844 lines. Contains routes, NLP logic, model loading, diffing algorithms, offset mapping, pipeline orchestration, Quran search integration, and CORS — all in one file. |
|
| 246 |
+
| **Duplicated directional blocks** | Medium | `_DIRECTIONAL_BLOCKS` in `app.py` duplicates logic that also exists in `araspell_rules.py`. |
|
| 247 |
+
| **12+ test files at project root** | Low | `test_proof.py`, `test_sv.py`, `test_pc.py`, etc. scattered in root instead of `tests/`. |
|
| 248 |
+
| **Dead code** | Low | `ENABLE_DIALECT_MODEL = False`, `ENABLE_AUTOCOMPLETE_MODEL = False` flags in `app.py` — no code path checks them for these features since they use lazy-loading. |
|
| 249 |
+
| **Archive directory** | Low | `archive/legacy_scripts/` contains old code that shouldn't ship in Docker image. |
|
| 250 |
+
|
| 251 |
+
### Frontend (Website)
|
| 252 |
+
|
| 253 |
+
| Item | Severity | Details |
|
| 254 |
+
|------|----------|---------|
|
| 255 |
+
| **`api.js` dead exports** | Medium | `export async function analyzeText()` — never imported. Website uses inline `fetch()` in `editor.js`. |
|
| 256 |
+
| **Tight coupling in `editor.js`** | Medium | DOM manipulation, API calls, suggestion management, and UI updates all in one 29KB file. |
|
| 257 |
+
| **No build system** | Low | No bundler, no tree-shaking, no code-splitting. All JS loaded via `<script>` tags. |
|
| 258 |
+
| **CSS structure** | Low | Single `components.css` at 4,125+ lines. No CSS modules, no scoping. |
|
| 259 |
+
|
| 260 |
+
### Extension
|
| 261 |
+
|
| 262 |
+
| Item | Severity | Details |
|
| 263 |
+
|------|----------|---------|
|
| 264 |
+
| **`popup.js` and `sidepanel.js` code duplication** | High | ~60% identical code: `updateCounts()`, `markStale()`, `setLoading()`, `updateScore()`, `renderSuggestions()`, `showToast()`. |
|
| 265 |
+
| **Missing API functions in `bayan-api.js`** | High | SidePanel calls `bayanAutocomplete()`, `bayanDialect()`, `bayanQuran()` which are not defined in `bayan-api.js`. These must be defined elsewhere or will throw. |
|
| 266 |
+
| **No TypeScript / JSDoc validation** | Low | All extension code is plain JS with no compile-time checking. |
|
| 267 |
+
|
| 268 |
+
---
|
| 269 |
+
|
| 270 |
+
## 10. Recommended Roadmap
|
| 271 |
+
|
| 272 |
+
### Phase 1: Security Hardening ⚡ (Critical — Before Any Growth)
|
| 273 |
+
|
| 274 |
+
**Timeline: 1-2 days**
|
| 275 |
+
|
| 276 |
+
1. **Remove `.env` from Git history** — `git filter-branch` or BFG Repo Cleaner
|
| 277 |
+
2. **Restrict CORS** — Change `origins: "*"` to allowlist `["https://bayan10-bayan-api.hf.space", "chrome-extension://<ext-id>"]`
|
| 278 |
+
3. **Add rate limiting** — Flask-Limiter: 30 req/min per IP for `/api/analyze`, 10 req/min for `/api/summarize`
|
| 279 |
+
4. **Disable debug endpoint in production** — Guard `/api/debug-models` behind `app.debug` flag
|
| 280 |
+
5. **Add Supabase RLS for `documents` table** — `CREATE POLICY ... USING (auth.uid() = user_id)`
|
| 281 |
+
|
| 282 |
+
### Phase 2: Extension Auth Unification 🔐 (High)
|
| 283 |
+
|
| 284 |
+
**Timeline: 3-5 days**
|
| 285 |
+
|
| 286 |
+
1. **Implement Supabase client in extension** — Add `@supabase/supabase-js` as UMD bundle in `shared/`
|
| 287 |
+
2. **Auth flow**: Use `chrome.identity.launchWebAuthFlow()` for Google OAuth → receive tokens → init Supabase session
|
| 288 |
+
3. **Session persistence**: Store refresh token in `chrome.storage.local`
|
| 289 |
+
4. **Auth sync**: When user logs in on website, broadcast via `postMessage` → content script → `chrome.storage`
|
| 290 |
+
5. **Result**: Extension users can access their documents, settings, and history
|
| 291 |
+
|
| 292 |
+
### Phase 3: Extension Feature Parity 🔧 (High)
|
| 293 |
+
|
| 294 |
+
**Timeline: 3-5 days**
|
| 295 |
+
|
| 296 |
+
1. **Add missing API functions** to `bayan-api.js`: `bayanAutocomplete()`, `bayanDialect()`, `bayanQuran()`
|
| 297 |
+
2. **Add autocomplete/dialect/quran tabs to popup** (currently SidePanel-only)
|
| 298 |
+
3. **Inline ghost text for content script** — Port `autocomplete.js` logic for textareas on 3rd-party sites
|
| 299 |
+
4. **Add basic export** — "Copy corrected text" button already exists; add "Download as TXT"
|
| 300 |
+
|
| 301 |
+
### Phase 4: Backend Refactoring 🏗️ (Medium)
|
| 302 |
+
|
| 303 |
+
**Timeline: 5-7 days**
|
| 304 |
+
|
| 305 |
+
1. **Split `app.py`** into:
|
| 306 |
+
- `routes/analyze.py`, `routes/summarize.py`, `routes/dialect.py`, `routes/quran.py`
|
| 307 |
+
- `services/pipeline.py` (orchestration)
|
| 308 |
+
- `middleware/cors.py`, `middleware/rate_limit.py`
|
| 309 |
+
2. **Create `002_documents.sql` migration** with proper RLS
|
| 310 |
+
3. **Move root-level test files** into `tests/`
|
| 311 |
+
4. **Remove `archive/` from Docker build** (add to `.dockerignore`)
|
| 312 |
+
|
| 313 |
+
### Phase 5: Extension Code Quality 🧹 (Medium)
|
| 314 |
+
|
| 315 |
+
**Timeline: 3-4 days**
|
| 316 |
+
|
| 317 |
+
1. **Extract shared logic** from `popup.js` and `sidepanel.js` into `shared/bayan-core.js`
|
| 318 |
+
2. **Add English locale** `_locales/en/messages.json`
|
| 319 |
+
3. **Add `all_frames: true`** to manifest for iframe editor support
|
| 320 |
+
4. **Add theme toggle** to popup and sidepanel
|
| 321 |
+
|
| 322 |
+
### Phase 6: Performance & Polish ✨ (Low)
|
| 323 |
+
|
| 324 |
+
**Timeline: 2-3 days**
|
| 325 |
+
|
| 326 |
+
1. **Lazy-load vendor libs** (mammoth, docx, html2canvas) on first use
|
| 327 |
+
2. **Add website-side API caching** (localStorage TTL cache like extension has)
|
| 328 |
+
3. **Add CSS/JS minification** to Docker build
|
| 329 |
+
4. **Add loading skeletons** for editor page
|
| 330 |
+
5. **Add onboarding flow** — sample text + guided tooltips
|
| 331 |
+
|
| 332 |
+
---
|
| 333 |
+
|
| 334 |
+
## Summary Matrix
|
| 335 |
+
|
| 336 |
+
| Category | Critical | High | Medium | Low | Total |
|
| 337 |
+
|----------|---------|------|--------|-----|-------|
|
| 338 |
+
| **Security** | 2 (S1, S2) | 1 (S3) | 2 (S4, S5) | 2 (S6, S7) | 7 |
|
| 339 |
+
| **Missing Features** | 0 | 5 (H1-H5) | 6 (M1-M6) | 4 (L1-L4) | 15 |
|
| 340 |
+
| **Bugs** | 0 | 1 (B3) | 2 (B1, B4) | 1 (B2) | 4 (+2 fixed) |
|
| 341 |
+
| **Performance** | 0 | 1 (P1) | 4 (P2-P5) | 2 (P6, P7) | 7 |
|
| 342 |
+
| **UX** | 0 | 0 | 3 (U1-U3) | 3 (U4-U6) | 6 |
|
| 343 |
+
| **Tech Debt** | 0 | 3 | 5 | 5 | 13 |
|
| 344 |
+
| **TOTAL** | **2** | **11** | **22** | **17** | **52** |
|
| 345 |
|
| 346 |
---
|
| 347 |
|
| 348 |
+
## Final Verdict
|
| 349 |
|
| 350 |
+
Bayan is a technically impressive product with a solid NLP pipeline, a mature editor engine, and a well-architected extension. The core correction features (Spelling → Grammar → Punctuation) work end-to-end across both surfaces.
|
|
|
|
|
|
|
| 351 |
|
| 352 |
+
**What Bayan does well:**
|
| 353 |
+
- ✅ Custom contenteditable editor with proper cursor handling
|
| 354 |
+
- ✅ Multi-stage NLP pipeline with offset mapping
|
| 355 |
+
- ✅ Extension uses overlay-only rendering (never modifies user DOM)
|
| 356 |
+
- ✅ Supabase integration for cloud persistence
|
| 357 |
+
- ✅ Comprehensive test coverage (16 backend test files)
|
| 358 |
+
- ✅ Extension follows MV3 best practices (service worker, side panel)
|
| 359 |
|
| 360 |
+
**What must be fixed before growth:**
|
| 361 |
+
1. 🔴 **Security**: CORS wildcard + no rate limiting = anyone can abuse the API
|
| 362 |
+
2. 🔴 **Auth gap**: Extension users can't persist anything — breaks the SaaS value proposition
|
| 363 |
+
3. 🟡 **Extension missing API functions**: `bayanAutocomplete/Dialect/Quran` will throw `ReferenceError`
|
| 364 |
+
4. 🟡 **Backend monolith**: 2,844-line `app.py` is a maintenance bottleneck
|
| 365 |
|
| 366 |
+
**Bottom line:** Bayan is 80% of the way to a production-grade SaaS product. The remaining 20% is security hardening, extension auth, and code architecture — all achievable in 2-3 focused weeks.
|
|
|
|
|
|
extension/shared/supabase.min.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
requirements.txt
CHANGED
|
@@ -15,3 +15,4 @@ gunicorn
|
|
| 15 |
python-dotenv
|
| 16 |
gradio_client
|
| 17 |
rapidfuzz
|
|
|
|
|
|
| 15 |
python-dotenv
|
| 16 |
gradio_client
|
| 17 |
rapidfuzz
|
| 18 |
+
Flask-Limiter
|
src/app.py
CHANGED
|
@@ -887,9 +887,11 @@ def _is_small_spelling_change(orig_word, corr_word, vocab_manager=None):
|
|
| 887 |
# AND roots have same length (true consonant swap, not a character addition)
|
| 888 |
# Exception: الولاد→الأولاد has roots ولاد(4)→أولاد(5) — different length = allow
|
| 889 |
_HAMZA_CHARS = set('أإآاء')
|
|
|
|
| 890 |
if (_o_root and _c_root and _o_root[0] != _c_root[0]
|
| 891 |
and len(_o_root) == len(_c_root) # same-length roots only
|
| 892 |
-
and not (_o_root[0] in _HAMZA_CHARS and _c_root[0] in _HAMZA_CHARS)
|
|
|
|
| 893 |
logger.info(
|
| 894 |
f"[SPELLING] Blocked first-letter change: '{orig_word}'→'{corr_word}' "
|
| 895 |
f"(root '{_o_root[0]}'→'{_c_root[0]}')"
|
|
|
|
| 887 |
# AND roots have same length (true consonant swap, not a character addition)
|
| 888 |
# Exception: الولاد→الأولاد has roots ولاد(4)→أولاد(5) — different length = allow
|
| 889 |
_HAMZA_CHARS = set('أإآاء')
|
| 890 |
+
_STOP_WORDS = {"التي", "الذي", "الذين", "هذا", "هذه", "هؤلاء", "تلك", "ذلك"}
|
| 891 |
if (_o_root and _c_root and _o_root[0] != _c_root[0]
|
| 892 |
and len(_o_root) == len(_c_root) # same-length roots only
|
| 893 |
+
and not (_o_root[0] in _HAMZA_CHARS and _c_root[0] in _HAMZA_CHARS)
|
| 894 |
+
and corr_word not in _STOP_WORDS):
|
| 895 |
logger.info(
|
| 896 |
f"[SPELLING] Blocked first-letter change: '{orig_word}'→'{corr_word}' "
|
| 897 |
f"(root '{_o_root[0]}'→'{_c_root[0]}')"
|
src/nlp/grammar/grammar_rules.py
CHANGED
|
@@ -9,6 +9,27 @@ from camel_tools.disambig.mle import MLEDisambiguator
|
|
| 9 |
|
| 10 |
logger = logging.getLogger(__name__)
|
| 11 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
class ArabicGrammarGuard:
|
| 14 |
def __init__(self):
|
|
@@ -201,25 +222,6 @@ class ArabicGrammarGuard:
|
|
| 201 |
# ── Batch 6: Noun-adjective gender agreement ──
|
| 202 |
# When a feminine noun is followed by a masculine adjective, add ة
|
| 203 |
# e.g. السيارة جميل → السيارة جميلة
|
| 204 |
-
KNOWN_FEMININE_NOUNS = {
|
| 205 |
-
'السيارة', 'سيارة', 'المدرسة', 'مدرسة', 'المدينة', 'مدينة',
|
| 206 |
-
'البنت', 'الشمس', 'الأرض', 'الطالبة', 'طالبة',
|
| 207 |
-
'الجامعة', 'جامعة', 'الشركة', 'شركة', 'الحكومة', 'حكومة',
|
| 208 |
-
'الغرفة', 'غرفة', 'الحديقة', 'حديقة', 'المكتبة', 'مكتبة',
|
| 209 |
-
'الدولة', 'دولة', 'الرحلة', 'رحلة', 'اللغة', 'لغة',
|
| 210 |
-
'القصة', 'قصة', 'الفكرة', 'فكرة', 'النتيجة', 'نتيجة',
|
| 211 |
-
}
|
| 212 |
-
# Common adjectives that have masculine/feminine pairs
|
| 213 |
-
MASC_TO_FEM_ADJ = {
|
| 214 |
-
'جميل': 'جميلة', 'كبير': 'كبيرة', 'صغير': 'صغيرة',
|
| 215 |
-
'طويل': 'طويلة', 'قصير': 'قصيرة', 'جديد': 'جديدة',
|
| 216 |
-
'قديم': 'قديمة', 'بعيد': 'بعيدة', 'قريب': 'قريبة',
|
| 217 |
-
'سريع': 'سريعة', 'بطيء': 'بطيئة', 'واسع': 'واسعة',
|
| 218 |
-
'ضيق': 'ضيقة', 'عميق': 'عميقة', 'خفيف': 'خفيفة',
|
| 219 |
-
'ثقيل': 'ثقيلة', 'نظيف': 'نظيفة', 'مشرق': 'مشرقة',
|
| 220 |
-
'ذكي': 'ذكية', 'غني': 'غنية', 'فقير': 'فقيرة',
|
| 221 |
-
'متفوق': 'متفوقة', 'مجتهد': 'مجتهدة', 'ممتاز': 'ممتازة',
|
| 222 |
-
}
|
| 223 |
words = text.split()
|
| 224 |
for i in range(len(words) - 1):
|
| 225 |
noun = words[i]
|
|
@@ -679,17 +681,6 @@ class ArabicGrammarGuard:
|
|
| 679 |
disambig_tokens = self.mle.disambiguate(tokens)
|
| 680 |
corrected_tokens = list(tokens)
|
| 681 |
|
| 682 |
-
MASC_TO_FEM_ADJ = {
|
| 683 |
-
'جميل': 'جميلة', 'كبير': 'كبيرة', 'صغير': 'صغيرة',
|
| 684 |
-
'طويل': 'طويلة', 'قصير': 'قصيرة', 'جديد': 'جديدة',
|
| 685 |
-
'قديم': 'قديمة', 'بعيد': 'بعيدة', 'قريب': 'قريبة',
|
| 686 |
-
'سريع': 'سريعة', 'بطيء': 'بطيئة', 'واسع': 'واسعة',
|
| 687 |
-
'ضيق': 'ضيقة', 'عميق': 'عميقة', 'خفيف': 'خفيفة',
|
| 688 |
-
'ثقيل': 'ثقيلة', 'نظيف': 'نظيفة', 'مشرق': 'مشرقة',
|
| 689 |
-
'ذكي': 'ذكية', 'غني': 'غنية', 'فقير': 'فقيرة',
|
| 690 |
-
'متفوق': 'متفوقة', 'مجتهد': 'مجتهدة', 'ممتاز': 'ممتازة',
|
| 691 |
-
}
|
| 692 |
-
|
| 693 |
for i in range(len(disambig_tokens) - 1):
|
| 694 |
w1 = corrected_tokens[i]
|
| 695 |
w2 = corrected_tokens[i+1]
|
|
|
|
| 9 |
|
| 10 |
logger = logging.getLogger(__name__)
|
| 11 |
|
| 12 |
+
KNOWN_FEMININE_NOUNS = {
|
| 13 |
+
'السيارة', 'سيارة', 'المدرسة', 'مدرسة', 'المدينة', 'مدينة',
|
| 14 |
+
'البنت', 'الشمس', 'الأرض', 'الطالبة', 'طالبة',
|
| 15 |
+
'الجامعة', 'جامعة', 'الشركة', 'شركة', 'الحكومة', 'حكومة',
|
| 16 |
+
'الغرفة', 'غرفة', 'الحديقة', 'حديقة', 'المكتبة', 'مكتبة',
|
| 17 |
+
'الدولة', 'دولة', 'الرحلة', 'رحلة', 'اللغة', 'لغة',
|
| 18 |
+
'القصة', 'قصة', 'الفكرة', 'فكرة', 'النتيجة', 'نتيجة',
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
# Common adjectives that have masculine/feminine pairs
|
| 22 |
+
MASC_TO_FEM_ADJ = {
|
| 23 |
+
'جميل': 'جميلة', 'كبير': 'كبيرة', 'صغير': 'صغيرة',
|
| 24 |
+
'طويل': 'طويلة', 'قصير': 'قصيرة', 'جديد': 'جديدة',
|
| 25 |
+
'قديم': 'قديمة', 'بعيد': 'بعيدة', 'قريب': 'قريبة',
|
| 26 |
+
'سريع': 'سريعة', 'بطيء': 'بطيئة', 'واسع': 'واسعة',
|
| 27 |
+
'ضيق': 'ضيقة', 'عميق': 'عميقة', 'خفيف': 'خفيفة',
|
| 28 |
+
'ثقيل': 'ثقيلة', 'نظيف': 'نظيفة', 'مشرق': 'مشرقة',
|
| 29 |
+
'ذكي': 'ذكية', 'غني': 'غنية', 'فقير': 'فقيرة',
|
| 30 |
+
'متفوق': 'متفوقة', 'مجتهد': 'مجتهدة', 'ممتاز': 'ممتازة',
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
|
| 34 |
class ArabicGrammarGuard:
|
| 35 |
def __init__(self):
|
|
|
|
| 222 |
# ── Batch 6: Noun-adjective gender agreement ──
|
| 223 |
# When a feminine noun is followed by a masculine adjective, add ة
|
| 224 |
# e.g. السيارة جميل → السيارة جميلة
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 225 |
words = text.split()
|
| 226 |
for i in range(len(words) - 1):
|
| 227 |
noun = words[i]
|
|
|
|
| 681 |
disambig_tokens = self.mle.disambiguate(tokens)
|
| 682 |
corrected_tokens = list(tokens)
|
| 683 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 684 |
for i in range(len(disambig_tokens) - 1):
|
| 685 |
w1 = corrected_tokens[i]
|
| 686 |
w2 = corrected_tokens[i+1]
|
src/nlp/punctuation/punctuation_rules.py
CHANGED
|
@@ -52,6 +52,18 @@ def arabic_postprocessing(text: str) -> str:
|
|
| 52 |
text = re.sub(r'،؛|؛،', '؛', text)
|
| 53 |
text = re.sub(r'([!؟])\.', r'\1', text)
|
| 54 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
# 6. Remove stray leading punctuation
|
| 56 |
text = re.sub(r'^[،؛:!؟. \t]+', '', text)
|
| 57 |
|
|
@@ -155,48 +167,59 @@ def validate_punctuation_diff(diff: dict, full_text: str = '') -> bool:
|
|
| 155 |
corr_no_punct = re.sub(r'[.,،؛؟!:;?!]+$', '', correction)
|
| 156 |
if _normalize_for_comparison(orig_no_punct.replace(' ', '')) == \
|
| 157 |
_normalize_for_comparison(corr_no_punct.replace(' ', '')):
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
# V2 FIX-30: Allow for 1+ words (not 5)
|
| 170 |
-
if _full_word_count >= 1 and not _full_already_has_terminal and not _full_has_ellipsis:
|
| 171 |
-
# ── Softened FIX-29 (Merged): Short-text ؟/! guard ──
|
| 172 |
-
# For short texts (< 3 words), block ؟ and ! unless
|
| 173 |
-
# cue words are present. Prevents "محمد" → "محمد؟"
|
| 174 |
-
# but allows "اليوم" → "اليوم." (period is safe).
|
| 175 |
-
# For 3+ words, allow freely (V2 behavior).
|
| 176 |
-
_added_punct = correction[len(orig_stripped):]
|
| 177 |
-
if _full_word_count < 3 and ('!' in _added_punct or '؟' in _added_punct):
|
| 178 |
-
_text_to_scan = full_text if full_text else original
|
| 179 |
-
_has_cue = any(w in _EXCL_CUES for w in _text_to_scan.split())
|
| 180 |
-
if not _has_cue:
|
| 181 |
-
logger.info(
|
| 182 |
-
f"[PUNC-SAFETY] Blocked !/؟ on short text without cue: "
|
| 183 |
-
f"'{original}' → '{correction}'"
|
| 184 |
-
)
|
| 185 |
-
return False
|
| 186 |
-
|
| 187 |
-
logger.info(
|
| 188 |
-
f"[PUNC-SAFETY] Allowed terminal punct for sentence "
|
| 189 |
-
f"({_full_word_count} words): "
|
| 190 |
-
f"'{original}' → '{correction}'"
|
| 191 |
-
)
|
| 192 |
-
# Fall through to remaining rules (don't return yet)
|
| 193 |
else:
|
| 194 |
-
#
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
|
| 201 |
|
| 202 |
|
|
|
|
| 52 |
text = re.sub(r'،؛|؛،', '؛', text)
|
| 53 |
text = re.sub(r'([!؟])\.', r'\1', text)
|
| 54 |
|
| 55 |
+
# 5.5 Syntactic context fixes for model hallucinations
|
| 56 |
+
# Remove colons/semicolons before relative pronouns
|
| 57 |
+
text = re.sub(r'[؛:]\s*(التي|الذي|الذين|اللتان|اللذان|اللاتي|اللواتي)', r' \1', text)
|
| 58 |
+
|
| 59 |
+
# Fix misplaced colons for saying verbs (e.g. قال: المعلم -> قال المعلم:)
|
| 60 |
+
text = re.sub(r'\b(قال|يقول|قالت|تقول|أجاب|أجابت|سأل|سألت|أخبر|أخبرت|صرح|صرحت|أضاف|أضافت|أردف|أردفت):?\s+(ال[أ-ي]+|أحمد|محمد|محمود|علي|عمر|خالد|فاطمة|مريم|عائشة|خديجة)\b:?', r'\1 \2:', text)
|
| 61 |
+
|
| 62 |
+
# Remove colons after verbs that do not introduce speech/lists
|
| 63 |
+
text = re.sub(r'\b(يقدر|يستطيع|يمكن|يجب|ينبغي|يعتبر|يعد|يرى|يعتقد)\s*:', r'\1 ', text)
|
| 64 |
+
# Replace semicolon with comma if followed by "و" (and) or similar conjunctions, as semicolon is for separate clauses
|
| 65 |
+
text = re.sub(r'؛\s*(و|ف|ثم|أو|أم|بل)\b', r'، \1', text)
|
| 66 |
+
|
| 67 |
# 6. Remove stray leading punctuation
|
| 68 |
text = re.sub(r'^[،؛:!؟. \t]+', '', text)
|
| 69 |
|
|
|
|
| 167 |
corr_no_punct = re.sub(r'[.,،؛؟!:;?!]+$', '', correction)
|
| 168 |
if _normalize_for_comparison(orig_no_punct.replace(' ', '')) == \
|
| 169 |
_normalize_for_comparison(corr_no_punct.replace(' ', '')):
|
| 170 |
+
|
| 171 |
+
is_at_end = False
|
| 172 |
+
if full_text and 'end' in diff:
|
| 173 |
+
is_at_end = diff['end'] >= len(full_text) - 2
|
| 174 |
+
elif not full_text:
|
| 175 |
+
is_at_end = True # If no context, assume it's a standalone fragment
|
| 176 |
+
|
| 177 |
+
if not is_at_end:
|
| 178 |
+
# Mid-sentence punctuation addition. This is safe to fall through to other rules.
|
| 179 |
+
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
else:
|
| 181 |
+
# This is a pure terminal-punctuation addition.
|
| 182 |
+
# V2 FIX-30: Fall back to original when full_text is empty
|
| 183 |
+
_word_count_source = full_text if full_text else original
|
| 184 |
+
_full_word_count = len(re.findall(
|
| 185 |
+
r'[\u0600-\u06FFa-zA-Z]+', _word_count_source
|
| 186 |
+
))
|
| 187 |
+
_full_already_has_terminal = bool(
|
| 188 |
+
re.search(r'[.،؛؟!?!][\s]*$', full_text)
|
| 189 |
+
) if full_text else False
|
| 190 |
+
_full_has_ellipsis = full_text.rstrip().endswith('...') if full_text else False
|
| 191 |
+
|
| 192 |
+
# V2 FIX-30: Allow for 1+ words (not 5)
|
| 193 |
+
if _full_word_count >= 1 and not _full_already_has_terminal and not _full_has_ellipsis:
|
| 194 |
+
# ── Softened FIX-29 (Merged): Short-text ؟/! guard ──
|
| 195 |
+
# For short texts (< 3 words), block ؟ and ! unless
|
| 196 |
+
# cue words are present. Prevents "محمد" → "محمد؟"
|
| 197 |
+
# but allows "اليوم" → "اليوم." (period is safe).
|
| 198 |
+
# For 3+ words, allow freely (V2 behavior).
|
| 199 |
+
_added_punct = correction[len(orig_stripped):]
|
| 200 |
+
if _full_word_count < 3 and ('!' in _added_punct or '؟' in _added_punct):
|
| 201 |
+
_text_to_scan = full_text if full_text else original
|
| 202 |
+
_has_cue = any(w in _EXCL_CUES for w in _text_to_scan.split())
|
| 203 |
+
if not _has_cue:
|
| 204 |
+
logger.info(
|
| 205 |
+
f"[PUNC-SAFETY] Blocked !/؟ on short text without cue: "
|
| 206 |
+
f"'{original}' → '{correction}'"
|
| 207 |
+
)
|
| 208 |
+
return False
|
| 209 |
+
|
| 210 |
+
logger.info(
|
| 211 |
+
f"[PUNC-SAFETY] Allowed terminal punct for sentence "
|
| 212 |
+
f"({_full_word_count} words): "
|
| 213 |
+
f"'{original}' → '{correction}'"
|
| 214 |
+
)
|
| 215 |
+
# Fall through to remaining rules (don't return yet)
|
| 216 |
+
else:
|
| 217 |
+
# Already has terminal punct or ends in ellipsis → REJECT
|
| 218 |
+
logger.info(
|
| 219 |
+
f"[PUNC-SAFETY] TerminalPunctuationGuard triggered: removing trailing punctuation "
|
| 220 |
+
f"'{original}' → '{correction}'"
|
| 221 |
+
)
|
| 222 |
+
return False
|
| 223 |
|
| 224 |
|
| 225 |
|
src/nlp/punctuation/punctuation_service.py
CHANGED
|
@@ -53,8 +53,14 @@ class PunctuationChecker:
|
|
| 53 |
if the model changed the BASE text (not just added/moved punctuation),
|
| 54 |
revert to the original word but keep any punctuation the model added.
|
| 55 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
orig_words = original.split()
|
| 57 |
-
punc_words =
|
| 58 |
|
| 59 |
if not orig_words or not punc_words:
|
| 60 |
return punctuated
|
|
@@ -72,11 +78,20 @@ class PunctuationChecker:
|
|
| 72 |
p_base = self._strip_punct(p_word)
|
| 73 |
|
| 74 |
if o_base == p_base:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
# Same base word — keep punctuation changes from model
|
| 76 |
result.append(p_word)
|
| 77 |
oi += 1
|
| 78 |
pi += 1
|
| 79 |
elif self._is_only_punct_difference(o_word, p_word):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
# Words differ only by punctuation — keep model's punctuation
|
| 81 |
result.append(p_word)
|
| 82 |
oi += 1
|
|
@@ -99,6 +114,10 @@ class PunctuationChecker:
|
|
| 99 |
|
| 100 |
# Only add punctuation that wasn't already there
|
| 101 |
if not o_word.endswith(punct_suffix) and punct_suffix:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
result.append(o_word + punct_suffix)
|
| 103 |
elif punct_prefix and not o_word.startswith(punct_prefix):
|
| 104 |
result.append(punct_prefix + o_word)
|
|
@@ -116,6 +135,10 @@ class PunctuationChecker:
|
|
| 116 |
while pi < len(punc_words):
|
| 117 |
p_word = punc_words[pi]
|
| 118 |
if all(ch in self.PUNCTUATION_CHARS or ch.isspace() for ch in p_word):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
result.append(p_word)
|
| 120 |
pi += 1
|
| 121 |
|
|
|
|
| 53 |
if the model changed the BASE text (not just added/moved punctuation),
|
| 54 |
revert to the original word but keep any punctuation the model added.
|
| 55 |
"""
|
| 56 |
+
# CRITICAL FIX: The model sometimes predicts standalone punctuation tokens (e.g. "المعلم :").
|
| 57 |
+
# If we split by space, the arrays will misalign. We MUST attach punctuation to the preceding word
|
| 58 |
+
# before splitting to ensure 1-to-1 alignment.
|
| 59 |
+
import re
|
| 60 |
+
punctuated_normalized = re.sub(r'\s+([،؛:!؟.])', r'\1', punctuated)
|
| 61 |
+
|
| 62 |
orig_words = original.split()
|
| 63 |
+
punc_words = punctuated_normalized.split()
|
| 64 |
|
| 65 |
if not orig_words or not punc_words:
|
| 66 |
return punctuated
|
|
|
|
| 78 |
p_base = self._strip_punct(p_word)
|
| 79 |
|
| 80 |
if o_base == p_base:
|
| 81 |
+
# Anti-hallucination for question marks
|
| 82 |
+
if '؟' in p_word and '؟' not in o_word:
|
| 83 |
+
_EXCL_CUES = {'هل', 'أين', 'متى', 'كيف', 'لماذا', 'ماذا', 'أي', 'كم', 'ما'}
|
| 84 |
+
if not any(w in _EXCL_CUES for w in orig_words):
|
| 85 |
+
p_word = p_word.replace('؟', '.')
|
| 86 |
# Same base word — keep punctuation changes from model
|
| 87 |
result.append(p_word)
|
| 88 |
oi += 1
|
| 89 |
pi += 1
|
| 90 |
elif self._is_only_punct_difference(o_word, p_word):
|
| 91 |
+
if '؟' in p_word and '؟' not in o_word:
|
| 92 |
+
_EXCL_CUES = {'هل', 'أين', 'متى', 'كيف', 'لماذا', 'ماذا', 'أي', 'كم', 'ما'}
|
| 93 |
+
if not any(w in _EXCL_CUES for w in orig_words):
|
| 94 |
+
p_word = p_word.replace('؟', '.')
|
| 95 |
# Words differ only by punctuation — keep model's punctuation
|
| 96 |
result.append(p_word)
|
| 97 |
oi += 1
|
|
|
|
| 114 |
|
| 115 |
# Only add punctuation that wasn't already there
|
| 116 |
if not o_word.endswith(punct_suffix) and punct_suffix:
|
| 117 |
+
if '؟' in punct_suffix and '؟' not in o_word:
|
| 118 |
+
_EXCL_CUES = {'هل', 'أين', 'متى', 'كيف', 'لماذا', 'ماذا', 'أي', 'كم', 'ما'}
|
| 119 |
+
if not any(w in _EXCL_CUES for w in orig_words):
|
| 120 |
+
punct_suffix = punct_suffix.replace('؟', '.')
|
| 121 |
result.append(o_word + punct_suffix)
|
| 122 |
elif punct_prefix and not o_word.startswith(punct_prefix):
|
| 123 |
result.append(punct_prefix + o_word)
|
|
|
|
| 135 |
while pi < len(punc_words):
|
| 136 |
p_word = punc_words[pi]
|
| 137 |
if all(ch in self.PUNCTUATION_CHARS or ch.isspace() for ch in p_word):
|
| 138 |
+
if '؟' in p_word:
|
| 139 |
+
_EXCL_CUES = {'هل', 'أين', 'متى', 'كيف', 'لماذا', 'ماذا', 'أي', 'كم', 'ما'}
|
| 140 |
+
if not any(w in _EXCL_CUES for w in orig_words):
|
| 141 |
+
p_word = p_word.replace('؟', '.')
|
| 142 |
result.append(p_word)
|
| 143 |
pi += 1
|
| 144 |
|