Commit ·
fe1e225
0
Parent(s):
HF Deploy: Fix syntax error with smart quotes in popup.js
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +2 -0
- .github/workflows/deploy.yml +85 -0
- .gitignore +0 -0
- 1.png +3 -0
- BAYAN_COMPLETE_AUDIT.md +366 -0
- Dockerfile +95 -0
- LICENSE +21 -0
- LOGOS/icon128.png +3 -0
- LOGOS/icon16.png +3 -0
- LOGOS/icon32.png +3 -0
- LOGOS/icon48.png +3 -0
- PROJECT_DESCRIPTION.md +217 -0
- Procfile +1 -0
- QUICKSTART.md +126 -0
- README.md +41 -0
- README_HF.md +8 -0
- README_SETUP.md +172 -0
- READMEquran.md +64 -0
- add_divider.py +19 -0
- add_extension_theme_toggle.py +124 -0
- analyze_failures.py +31 -0
- apply_locks.py +77 -0
- archive/legacy_scripts/AraSpell.py +2224 -0
- archive/legacy_scripts/Grammer_Rules.py +179 -0
- archive/legacy_scripts/PuncAra.py +180 -0
- archive/legacy_scripts/gradio Spelling.py +210 -0
- archive/legacy_scripts/punctuation_rulesV2.py +257 -0
- archive/old_tests/deep_dive_expanded.json +1323 -0
- archive/old_tests/deep_dive_expanded.py +428 -0
- archive/old_tests/deep_dive_gaps.json +260 -0
- archive/old_tests/deep_dive_gaps.py +295 -0
- archive/old_tests/deep_dive_output.json +671 -0
- archive/old_tests/deep_dive_test.py +519 -0
- archive/old_tests/gap_filling_results.json +261 -0
- archive/old_tests/gap_filling_tests.py +522 -0
- archive/old_tests/phase0_investigation.py +221 -0
- archive/old_tests/phase0_results.json +75 -0
- archive/old_tests/phase10_helpers/audit_output.txt +339 -0
- archive/old_tests/phase10_helpers/audit_script.py +71 -0
- archive/old_tests/phase10_helpers/extract_entity_results.py +16 -0
- archive/old_tests/phase10_helpers/fetch_hf_logs.py +89 -0
- archive/old_tests/phase10_helpers/generate_audit_md.py +165 -0
- archive/old_tests/phase10_helpers/generate_collision_dataset.py +48 -0
- archive/old_tests/phase10_helpers/generate_regression_audit.py +231 -0
- archive/old_tests/phase10_helpers/show_samples.py +39 -0
- archive/old_tests/phase5_investigation.py +161 -0
- archive/old_tests/phase5_results.json +61 -0
- archive/old_tests/phase8_adversarial_audit.py +678 -0
- archive/old_tests/phase9_results.json +0 -0
- archive/old_tests/phase9_validation.py +811 -0
.gitattributes
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.db filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
quran_master.db filter=lfs diff=lfs merge=lfs -text
|
.github/workflows/deploy.yml
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Bayan CI/CD
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
branches: [main]
|
| 6 |
+
pull_request:
|
| 7 |
+
branches: [main]
|
| 8 |
+
|
| 9 |
+
jobs:
|
| 10 |
+
validate:
|
| 11 |
+
name: Validate Code
|
| 12 |
+
runs-on: ubuntu-latest
|
| 13 |
+
steps:
|
| 14 |
+
- uses: actions/checkout@v4
|
| 15 |
+
|
| 16 |
+
- name: Setup Python
|
| 17 |
+
uses: actions/setup-python@v5
|
| 18 |
+
with:
|
| 19 |
+
python-version: '3.12'
|
| 20 |
+
|
| 21 |
+
- name: Check Python syntax (all .py files)
|
| 22 |
+
run: |
|
| 23 |
+
echo "Checking Python syntax..."
|
| 24 |
+
find . -name "*.py" -not -path "./.git/*" -not -path "./archive/*" | while read f; do
|
| 25 |
+
python -m py_compile "$f" 2>&1 && echo " ✅ $f" || { echo " ❌ $f"; exit 1; }
|
| 26 |
+
done
|
| 27 |
+
echo "✅ All Python files have valid syntax"
|
| 28 |
+
|
| 29 |
+
- name: Verify critical files exist
|
| 30 |
+
run: |
|
| 31 |
+
for f in src/app.py src/model_loader.py src/hf_inference.py src/index.html \
|
| 32 |
+
src/nlp/__init__.py src/nlp/spelling/araspell_service.py \
|
| 33 |
+
src/nlp/grammar/grammar_service.py src/nlp/punctuation/punctuation_service.py \
|
| 34 |
+
Dockerfile Procfile requirements.txt; do
|
| 35 |
+
test -f "$f" && echo " ✅ $f" || { echo " ❌ MISSING: $f"; exit 1; }
|
| 36 |
+
done
|
| 37 |
+
echo "✅ All critical files present"
|
| 38 |
+
|
| 39 |
+
- name: Verify API routes defined in app.py
|
| 40 |
+
run: |
|
| 41 |
+
for route in "/api/health" "/api/analyze" "/api/summarize" "/api/spelling" \
|
| 42 |
+
"/api/grammar" "/api/punctuation" "/api/quran"; do
|
| 43 |
+
grep -q "$route" src/app.py && echo " ✅ $route" || { echo " ❌ MISSING ROUTE: $route"; exit 1; }
|
| 44 |
+
done
|
| 45 |
+
echo "✅ All API routes defined"
|
| 46 |
+
|
| 47 |
+
- name: Validate Supabase meta tags in index.html
|
| 48 |
+
run: |
|
| 49 |
+
grep -q 'supabase-url' src/index.html && echo " ✅ supabase-url meta tag" || exit 1
|
| 50 |
+
grep -q 'supabase-anon-key' src/index.html && echo " ✅ supabase-anon-key meta tag" || exit 1
|
| 51 |
+
echo "✅ Supabase tags present"
|
| 52 |
+
|
| 53 |
+
- name: Validate Dockerfile
|
| 54 |
+
run: |
|
| 55 |
+
grep -q 'EXPOSE' Dockerfile && echo " ✅ EXPOSE directive" || exit 1
|
| 56 |
+
grep -q 'gunicorn\|CMD' Dockerfile && echo " ✅ Startup command" || exit 1
|
| 57 |
+
echo "✅ Dockerfile valid"
|
| 58 |
+
|
| 59 |
+
health-check:
|
| 60 |
+
name: Post-Deploy Health Check
|
| 61 |
+
needs: validate
|
| 62 |
+
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
| 63 |
+
runs-on: ubuntu-latest
|
| 64 |
+
steps:
|
| 65 |
+
- name: Wait for HuggingFace Spaces to deploy
|
| 66 |
+
run: sleep 120
|
| 67 |
+
|
| 68 |
+
- name: Check backend health
|
| 69 |
+
run: |
|
| 70 |
+
HEALTH_URL="${{ secrets.BACKEND_URL }}/api/health"
|
| 71 |
+
if [ -z "${{ secrets.BACKEND_URL }}" ]; then
|
| 72 |
+
HEALTH_URL="https://bayan10-bayan-api.hf.space/api/health"
|
| 73 |
+
fi
|
| 74 |
+
echo "Checking: $HEALTH_URL"
|
| 75 |
+
response=$(curl -s -w "\n%{http_code}" "$HEALTH_URL")
|
| 76 |
+
http_code=$(echo "$response" | tail -n1)
|
| 77 |
+
body=$(echo "$response" | head -n -1)
|
| 78 |
+
echo "Status: $http_code"
|
| 79 |
+
echo "Body: $body"
|
| 80 |
+
if [ "$http_code" = "200" ] || [ "$http_code" = "503" ]; then
|
| 81 |
+
echo "✅ Backend is responding"
|
| 82 |
+
else
|
| 83 |
+
echo "❌ Backend health check failed"
|
| 84 |
+
exit 1
|
| 85 |
+
fi
|
.gitignore
ADDED
|
Binary file (556 Bytes). View file
|
|
|
1.png
ADDED
|
Git LFS Details
|
BAYAN_COMPLETE_AUDIT.md
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
Dockerfile
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.12-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# Install system dependencies
|
| 6 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 7 |
+
build-essential \
|
| 8 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 9 |
+
|
| 10 |
+
# Copy requirements and install Python dependencies
|
| 11 |
+
# Install CPU-only PyTorch first (saves ~1.5GB vs full torch with CUDA)
|
| 12 |
+
COPY requirements.txt .
|
| 13 |
+
RUN pip install --no-cache-dir torch --index-url https://download.pytorch.org/whl/cpu && \
|
| 14 |
+
pip install --no-cache-dir -r requirements.txt
|
| 15 |
+
|
| 16 |
+
# Pre-download models during build (network is available here)
|
| 17 |
+
# At runtime, the container has NO outbound DNS, so models must be cached
|
| 18 |
+
|
| 19 |
+
# Set HF_HOME to a global path so non-root users (like in HF Spaces) can access cached models
|
| 20 |
+
ENV HF_HOME=/opt/huggingface
|
| 21 |
+
RUN mkdir -p /opt/huggingface && chmod 777 /opt/huggingface
|
| 22 |
+
# 1. Summarization model (MBart, float16)
|
| 23 |
+
RUN python -c "\
|
| 24 |
+
from transformers import MBartForConditionalGeneration, AutoTokenizer, AutoConfig; \
|
| 25 |
+
import torch; \
|
| 26 |
+
repo = 'bayan10/summarization-model'; \
|
| 27 |
+
print('Downloading summarization tokenizer...'); \
|
| 28 |
+
AutoTokenizer.from_pretrained(repo); \
|
| 29 |
+
print('Downloading summarization config...'); \
|
| 30 |
+
AutoConfig.from_pretrained(repo); \
|
| 31 |
+
print('Downloading summarization model (float16)...'); \
|
| 32 |
+
MBartForConditionalGeneration.from_pretrained(repo, torch_dtype=torch.float16); \
|
| 33 |
+
print('Summarization model cached!'); \
|
| 34 |
+
"
|
| 35 |
+
|
| 36 |
+
# 2. Spelling model (AraSpell — AraBERT encoder-decoder + checkpoint)
|
| 37 |
+
RUN python -c "\
|
| 38 |
+
from huggingface_hub import hf_hub_download; \
|
| 39 |
+
from transformers import AutoTokenizer, EncoderDecoderModel, AutoModelForMaskedLM; \
|
| 40 |
+
print('Downloading AraSpell checkpoint...'); \
|
| 41 |
+
hf_hub_download(repo_id='bayan10/AraSpell-Model', filename='last_model.pt'); \
|
| 42 |
+
print('Downloading AraBERT tokenizer...'); \
|
| 43 |
+
AutoTokenizer.from_pretrained('aubmindlab/bert-base-arabertv02'); \
|
| 44 |
+
print('Downloading AraBERT encoder-decoder...'); \
|
| 45 |
+
EncoderDecoderModel.from_encoder_decoder_pretrained('aubmindlab/bert-base-arabertv02', 'aubmindlab/bert-base-arabertv02'); \
|
| 46 |
+
print('Downloading AraBERT MLM (for ContextualCorrector)...'); \
|
| 47 |
+
AutoModelForMaskedLM.from_pretrained('aubmindlab/bert-base-arabertv02'); \
|
| 48 |
+
print('Spelling model + MLM cached!'); \
|
| 49 |
+
"
|
| 50 |
+
|
| 51 |
+
# 3. Grammar — camel-tools MLE disambiguator data
|
| 52 |
+
# Set CAMELTOOLS_DATA to a global path so non-root users (like in HF Spaces) can access it
|
| 53 |
+
ENV CAMELTOOLS_DATA=/opt/camel_tools
|
| 54 |
+
RUN mkdir -p /opt/camel_tools && chmod 777 /opt/camel_tools && camel_data -i light
|
| 55 |
+
|
| 56 |
+
# 4. Punctuation model (PuncAra-v1 — EncoderDecoderModel)
|
| 57 |
+
RUN python -c "\
|
| 58 |
+
from transformers import EncoderDecoderModel, AutoTokenizer; \
|
| 59 |
+
repo = 'bayan10/PuncAra-v1'; \
|
| 60 |
+
print('Downloading PuncAra-v1 tokenizer...'); \
|
| 61 |
+
AutoTokenizer.from_pretrained(repo); \
|
| 62 |
+
print('Downloading PuncAra-v1 model...'); \
|
| 63 |
+
EncoderDecoderModel.from_pretrained(repo); \
|
| 64 |
+
print('PuncAra-v1 cached!'); \
|
| 65 |
+
"
|
| 66 |
+
|
| 67 |
+
# 5. Dialect-to-MSA model (mT5, float16)
|
| 68 |
+
RUN python -c "\
|
| 69 |
+
from transformers import AutoTokenizer, AutoModelForSeq2SeqLM; \
|
| 70 |
+
import torch; \
|
| 71 |
+
repo = 'bayan10/dialect-to-msa-model'; \
|
| 72 |
+
print('Downloading dialect tokenizer...'); \
|
| 73 |
+
AutoTokenizer.from_pretrained(repo); \
|
| 74 |
+
print('Downloading dialect model (float16)...'); \
|
| 75 |
+
AutoModelForSeq2SeqLM.from_pretrained(repo, torch_dtype=torch.float16); \
|
| 76 |
+
print('Dialect model cached!'); \
|
| 77 |
+
"
|
| 78 |
+
|
| 79 |
+
# Copy application code
|
| 80 |
+
COPY src/ ./src/
|
| 81 |
+
COPY quran.py ./
|
| 82 |
+
COPY quran_master.db ./
|
| 83 |
+
COPY .env* ./
|
| 84 |
+
|
| 85 |
+
# Set environment variables
|
| 86 |
+
ENV PORT=7860
|
| 87 |
+
ENV DEBUG=False
|
| 88 |
+
ENV PYTHONUNBUFFERED=1
|
| 89 |
+
|
| 90 |
+
# Expose port
|
| 91 |
+
EXPOSE 7860
|
| 92 |
+
|
| 93 |
+
# Start the app with gunicorn (single worker to minimize RAM)
|
| 94 |
+
# Timeout 300s: full pipeline (spelling ~50s + grammar ~8s + punctuation ~30s + cold start)
|
| 95 |
+
CMD ["gunicorn", "--chdir", "src", "app:app", "--bind", "0.0.0.0:7860", "--timeout", "300", "--workers", "1"]
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2025 Your Organization
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
LOGOS/icon128.png
ADDED
|
|
Git LFS Details
|
LOGOS/icon16.png
ADDED
|
|
Git LFS Details
|
LOGOS/icon32.png
ADDED
|
|
Git LFS Details
|
LOGOS/icon48.png
ADDED
|
|
Git LFS Details
|
PROJECT_DESCRIPTION.md
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Bayan (بيان) - Arabic Writing Assistant & Text Summarization System
|
| 2 |
+
|
| 3 |
+
Bayan is a state-of-the-art Arabic text editing and summarization application. Similar to assistants like Grammarly, Bayan provides real-time correction of spelling, grammar, and punctuation, combined with autocomplete suggestions and an advanced summarization pipeline. It features a modern, responsive web interface that communicates with a Flask backend powered by deep learning models.
|
| 4 |
+
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
## 📁 Repository Layout & File Descriptions
|
| 8 |
+
|
| 9 |
+
```
|
| 10 |
+
Bayan/
|
| 11 |
+
├── data/ # Directory for raw and processed datasets (empty by default)
|
| 12 |
+
├── models/ # Deep learning models directory (organized by task)
|
| 13 |
+
│ ├── Autocomplete/ # GPT-2 autocomplete model
|
| 14 |
+
│ ├── Grammrar/ # Gemma-based grammar correction model
|
| 15 |
+
│ ├── Punctuation/ # Seq2Seq punctuation correction model
|
| 16 |
+
│ ├── Spelling/ # BERT-based spelling corrector checkpoint
|
| 17 |
+
│ └── Summarization/ # mBART summarization model checkpoint
|
| 18 |
+
├── src/ # Core backend source code and frontend
|
| 19 |
+
│ ├── app.py # Flask server containing API endpoints
|
| 20 |
+
│ ├── ara_spell.py # Custom spell-checking algorithms and post-processing
|
| 21 |
+
│ ├── index.html # TailwindCSS & Vanilla JS responsive web interface
|
| 22 |
+
│ ├── model_loader.py # Loader classes for all deep learning models
|
| 23 |
+
│ └── README.md # Source code instructions and API output contracts
|
| 24 |
+
├── check_dependencies.py # Helper script to check required Python libraries
|
| 25 |
+
├── inspect_decoder.py # Weight inspection helper for the spelling model
|
| 26 |
+
├── inspect_model.py # Basic PyTorch checkpoint architecture identifier
|
| 27 |
+
├── inspect_model_details.py # Detailed tensor shape explorer for spelling checkpoint
|
| 28 |
+
├── inspect_model_weights.py # Checkpoint structure explorer
|
| 29 |
+
├── LICENSE # MIT License
|
| 30 |
+
├── QUICKSTART.md # Quickstart guide for setting up and running Bayan
|
| 31 |
+
├── README.md # Main project overview and directory layout
|
| 32 |
+
├── README_SETUP.md # Detailed step-by-step setup and troubleshooting guide
|
| 33 |
+
├── reproduce_issue.py # Simple local script to test Spelling, Grammar, and Punctuation models
|
| 34 |
+
├── requirements.txt # Python dependencies (PyTorch, Transformers, Flask, etc.)
|
| 35 |
+
├── run_app.py # Standard launcher script for the application
|
| 36 |
+
├── summarization_test.py # Local tests and configuration options for Summarization
|
| 37 |
+
├── test_analyze_api.py # Request test script for the /api/analyze endpoint
|
| 38 |
+
├── test_analyze_methods.py # Request test script for GET/POST validations of analyze endpoint
|
| 39 |
+
├── test_model_load.py # Verification script for local summarization model loading
|
| 40 |
+
├── upload_model.py # Script to upload models to the Hugging Face hub
|
| 41 |
+
└── verify_api_live.py # Test script to send sample text to a live API server
|
| 42 |
+
```
|
| 43 |
+
|
| 44 |
+
---
|
| 45 |
+
|
| 46 |
+
## 🛠️ Core Features
|
| 47 |
+
|
| 48 |
+
1. **Smart Spelling Correction (`SpellingModel`)**:
|
| 49 |
+
- Cleans the text (removes harakat and tatweel), collapses repeated characters, and resolves common keyboard substitution errors.
|
| 50 |
+
- Generates candidates using seq2seq model inference (beams), smart rules-based heuristics, and edit-distance suggestions (Norvig's spelling corrector adapted for Arabic).
|
| 51 |
+
- Reranks candidates using a combined formula of **fluency** (evaluated using a BERT Masked Language Model), **similarity** (Damerau-Levenshtein distance), and **vocabulary-aware acceptance** (checks In-Vocabulary/Out-of-Vocabulary words from the tokenizer dictionary).
|
| 52 |
+
|
| 53 |
+
2. **Grammar Correction (`GrammarModel`)**:
|
| 54 |
+
- Loads a Gemma causal language model configured to run on CPU.
|
| 55 |
+
- Evaluates grammar through a standard chat template prompt.
|
| 56 |
+
- Extracts the first valid non-empty corrected sentence and rejects generic instruction text generated by the model.
|
| 57 |
+
|
| 58 |
+
3. **Punctuation Insertion (`PunctuationModel`)**:
|
| 59 |
+
- Uses a Seq2Seq architecture to automatically place Arabic commas (`،`), semicolons (`؛`), question marks (`؟`), periods (`.`), and quotation marks (`« »`) into continuous text.
|
| 60 |
+
|
| 61 |
+
4. **Text Summarization (`SummarizationModel`)**:
|
| 62 |
+
- Leverages an mBART conditional generation model.
|
| 63 |
+
- Supports variable length thresholds (short: ~30%, medium: ~50%, long: ~70% of the input text length).
|
| 64 |
+
- Features a **safe extractive fallback** mechanism: if the generated abstractive summary deviates too far from the original text (monitored by word overlap and similarity ratios), it falls back to a readable extractive summary composed of the opening sentences of the source text.
|
| 65 |
+
|
| 66 |
+
5. **Autocomplete Suggestions (`AutocompleteModel`)**:
|
| 67 |
+
- Powered by a local GPT-2 model (CPU-only mode) configured to predict the next word given a text prefix.
|
| 68 |
+
- Integrates with the web interface to display ghost text prompts that users can accept by pressing the `Tab` key.
|
| 69 |
+
|
| 70 |
+
---
|
| 71 |
+
|
| 72 |
+
## 🖥️ Architecture & Web Interface
|
| 73 |
+
|
| 74 |
+
The project uses a unified **Client-Server Architecture**:
|
| 75 |
+
|
| 76 |
+
```mermaid
|
| 77 |
+
graph TD
|
| 78 |
+
Client[Web Interface: HTML / CSS / JS] <-->|JSON over HTTP| Server[Flask API Server: app.py]
|
| 79 |
+
Server <--> ModelLoader[model_loader.py]
|
| 80 |
+
ModelLoader <--> Spelling[SpellingModel / ara_spell.py]
|
| 81 |
+
ModelLoader <--> Grammar[GrammarModel]
|
| 82 |
+
ModelLoader <--> Punctuation[PunctuationModel]
|
| 83 |
+
ModelLoader <--> Summarization[SummarizationModel]
|
| 84 |
+
ModelLoader <--> Autocomplete[AutocompleteModel]
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
### 1. Backend: Flask API (`src/app.py`)
|
| 88 |
+
- Manages model state instances and startup loading triggers (loads the summarization model on startup and lazily loads autocomplete as needed).
|
| 89 |
+
- Provides API endpoints validating text length requirements (between 10 and 5,000 characters).
|
| 90 |
+
- Implements `/api/analyze` which coordinates a sequential processing pipeline:
|
| 91 |
+
$$\text{Input Text} \rightarrow \text{Spelling Correction} \rightarrow \text{Grammar Correction} \rightarrow \text{Punctuation Insertion} \rightarrow \text{Diff Calculation}$$
|
| 92 |
+
|
| 93 |
+
### 2. Frontend: Modern Web Application (`src/index.html`)
|
| 94 |
+
- Built using **TailwindCSS** for styling, **Google Fonts** (Tajawal, Noto Kufi Arabic) for premium typography, and glassmorphism cards.
|
| 95 |
+
- Includes a live, rich editing canvas (`contenteditable`) with instant wavy underlines representing errors:
|
| 96 |
+
- <span style="border-bottom: 2px wavy #ef4444; background: rgba(239, 68, 68, 0.1); padding: 0 4px;">Red underlines</span> indicate **Spelling Errors**.
|
| 97 |
+
- <span style="border-bottom: 2px wavy #fbbf24; background: rgba(251, 191, 36, 0.1); padding: 0 4px;">Yellow underlines</span> indicate **Grammar / Punctuation Suggestions**.
|
| 98 |
+
- Features an interactive **suggestion tooltip** allowing users to click on highlighted words to view explanations and apply replacements directly.
|
| 99 |
+
- Displays a real-time **document score metric** (0–100 circular gauge) based on error density, along with word counters and feedback lists.
|
| 100 |
+
- Hosts a **Summarization Panel** where users can control the length and generation configuration of the text summarizer.
|
| 101 |
+
|
| 102 |
+
---
|
| 103 |
+
|
| 104 |
+
## 🔌 API Endpoints Reference
|
| 105 |
+
|
| 106 |
+
### 1. Health Check
|
| 107 |
+
* **Endpoint**: `GET /api/health`
|
| 108 |
+
* **Response**:
|
| 109 |
+
```json
|
| 110 |
+
{
|
| 111 |
+
"status": "healthy",
|
| 112 |
+
"models": {
|
| 113 |
+
"summarization": true,
|
| 114 |
+
"spelling": false,
|
| 115 |
+
"autocomplete": false,
|
| 116 |
+
"grammar": false,
|
| 117 |
+
"punctuation": false
|
| 118 |
+
}
|
| 119 |
+
}
|
| 120 |
+
```
|
| 121 |
+
|
| 122 |
+
### 2. Summarize Text
|
| 123 |
+
* **Endpoint**: `POST /api/summarize`
|
| 124 |
+
* **Payload**:
|
| 125 |
+
```json
|
| 126 |
+
{
|
| 127 |
+
"text": "النص العربي الطويل المراد تلخيصه...",
|
| 128 |
+
"length": 2, // 1 = short, 2 = medium, 3 = long
|
| 129 |
+
"full_text": true
|
| 130 |
+
}
|
| 131 |
+
```
|
| 132 |
+
* **Response**:
|
| 133 |
+
```json
|
| 134 |
+
{
|
| 135 |
+
"status": "success",
|
| 136 |
+
"summary": "الملخص المولد من النموذج...",
|
| 137 |
+
"original_length": 1420,
|
| 138 |
+
"summary_length": 620
|
| 139 |
+
}
|
| 140 |
+
```
|
| 141 |
+
|
| 142 |
+
### 3. Spelling Correction
|
| 143 |
+
* **Endpoint**: `POST /api/spelling`
|
| 144 |
+
* **Payload**: `{"text": "الكتبة الصحيحه"}`
|
| 145 |
+
* **Response**: `{"corrected": "الكتابة الصحيحة", "status": "success", ...}`
|
| 146 |
+
|
| 147 |
+
### 4. Autocomplete
|
| 148 |
+
* **Endpoint**: `POST /api/autocomplete`
|
| 149 |
+
* **Payload**: `{"text": "ذهب الطالب إلى", "n": 3}`
|
| 150 |
+
* **Response**: `{"suggestions": ["المدرسة", "الجامعة", "الفصل"], "status": "success"}`
|
| 151 |
+
|
| 152 |
+
### 5. Unified Analyze Text
|
| 153 |
+
* **Endpoint**: `POST /api/analyze`
|
| 154 |
+
* **Payload**: `{"text": "الطلاب ذهبو الى المدرسة"}`
|
| 155 |
+
* **Response**:
|
| 156 |
+
```json
|
| 157 |
+
{
|
| 158 |
+
"original": "الطلاب ذهبو الى المدرسة",
|
| 159 |
+
"corrected": "ذهب الطلاب إلى المدرسة.",
|
| 160 |
+
"suggestions": [
|
| 161 |
+
{
|
| 162 |
+
"original": "ذهبو",
|
| 163 |
+
"correction": "ذهبوا",
|
| 164 |
+
"type": "spelling"
|
| 165 |
+
},
|
| 166 |
+
{
|
| 167 |
+
"original": "ذهبوا",
|
| 168 |
+
"correction": "ذهب",
|
| 169 |
+
"type": "grammar"
|
| 170 |
+
},
|
| 171 |
+
{
|
| 172 |
+
"original": "الطلاب ذهب",
|
| 173 |
+
"correction": "ذهب الطلاب",
|
| 174 |
+
"type": "grammar"
|
| 175 |
+
},
|
| 176 |
+
{
|
| 177 |
+
"original": "المدرسة",
|
| 178 |
+
"correction": "المدرسة.",
|
| 179 |
+
"type": "punctuation"
|
| 180 |
+
}
|
| 181 |
+
],
|
| 182 |
+
"status": "success"
|
| 183 |
+
}
|
| 184 |
+
```
|
| 185 |
+
|
| 186 |
+
---
|
| 187 |
+
|
| 188 |
+
## 🚀 How to Run the Project
|
| 189 |
+
|
| 190 |
+
### 1. Install Dependencies
|
| 191 |
+
Make sure you have Python 3.8+ installed, and then run:
|
| 192 |
+
```bash
|
| 193 |
+
pip install -r requirements.txt
|
| 194 |
+
```
|
| 195 |
+
*Note: If you are running on a CPU-only environment or want to configure PyTorch for CUDA (GPU), visit [PyTorch Local Setup](https://pytorch.org/get-started/locally/) to install the appropriate distribution.*
|
| 196 |
+
|
| 197 |
+
### 2. Prepare Model Files
|
| 198 |
+
Verify that you have placed the model files under the `models/` directory:
|
| 199 |
+
- Summarization: `models/Summarization/Model/`
|
| 200 |
+
- Spelling: `models/Spelling/Model/`
|
| 201 |
+
- Autocomplete: `models/Autocomplete/Model/`
|
| 202 |
+
- Grammar: `models/Grammrar/Model/`
|
| 203 |
+
- Punctuation: `models/Punctuation/Model/`
|
| 204 |
+
|
| 205 |
+
### 3. Run the Server
|
| 206 |
+
Use gunicorn (production) or Flask dev server:
|
| 207 |
+
```bash
|
| 208 |
+
# Production (matches Procfile)
|
| 209 |
+
cd src && gunicorn app:app --bind 0.0.0.0:7860 --timeout 120 --workers 1
|
| 210 |
+
|
| 211 |
+
# Development
|
| 212 |
+
cd src && python -c "from app import app; app.run(host='0.0.0.0', port=7860, debug=True)"
|
| 213 |
+
```
|
| 214 |
+
Open your web browser and navigate to:
|
| 215 |
+
```
|
| 216 |
+
http://localhost:7860
|
| 217 |
+
```
|
Procfile
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
web: cd src && gunicorn app:app --bind 0.0.0.0:$PORT --timeout 120 --workers 1
|
QUICKSTART.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Bayan - Quick Start Guide
|
| 2 |
+
|
| 3 |
+
## 🚀 Quick Start
|
| 4 |
+
|
| 5 |
+
### 1. Install Dependencies
|
| 6 |
+
```bash
|
| 7 |
+
pip install -r requirements.txt
|
| 8 |
+
```
|
| 9 |
+
|
| 10 |
+
**Note:** If you have issues, install PyTorch separately:
|
| 11 |
+
- CPU: `pip install torch --index-url https://download.pytorch.org/whl/cpu`
|
| 12 |
+
- GPU: Visit https://pytorch.org/get-started/locally/
|
| 13 |
+
|
| 14 |
+
### 2. Run the Application
|
| 15 |
+
```bash
|
| 16 |
+
python run_app.py
|
| 17 |
+
```
|
| 18 |
+
|
| 19 |
+
### 3. Open in Browser
|
| 20 |
+
Navigate to: **http://localhost:5000**
|
| 21 |
+
|
| 22 |
+
## 📁 Project Structure
|
| 23 |
+
|
| 24 |
+
```
|
| 25 |
+
Bayan/
|
| 26 |
+
├── src/
|
| 27 |
+
│ ├── app.py # Flask backend server
|
| 28 |
+
│ ├── model_loader.py # Model loading and inference
|
| 29 |
+
│ └── index.html # Web interface
|
| 30 |
+
├── models/
|
| 31 |
+
│ └── arabic_summarization_model/
|
| 32 |
+
│ └── content/drive/MyDrive/arabic_summarization_model/
|
| 33 |
+
│ ├── config.json
|
| 34 |
+
│ ├── model.safetensors
|
| 35 |
+
│ └── ... (other model files)
|
| 36 |
+
├── run_app.py # Application launcher
|
| 37 |
+
├── requirements.txt # Python dependencies
|
| 38 |
+
└── README_SETUP.md # Detailed setup guide
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
## 🔧 Features
|
| 42 |
+
|
| 43 |
+
✅ **Robust Error Handling**
|
| 44 |
+
- Path validation for model files
|
| 45 |
+
- Graceful fallbacks if model loading fails
|
| 46 |
+
- Input validation and sanitization
|
| 47 |
+
- Clear error messages
|
| 48 |
+
|
| 49 |
+
✅ **Security**
|
| 50 |
+
- Input length limits (max 5000 characters)
|
| 51 |
+
- CORS enabled for web interface
|
| 52 |
+
- Safe model loading
|
| 53 |
+
- Error logging
|
| 54 |
+
|
| 55 |
+
✅ **User Experience**
|
| 56 |
+
- Loading indicators
|
| 57 |
+
- Real-time feedback
|
| 58 |
+
- Arabic language support
|
| 59 |
+
- Responsive design
|
| 60 |
+
|
| 61 |
+
## 🧪 Testing
|
| 62 |
+
|
| 63 |
+
### Test API Health
|
| 64 |
+
```bash
|
| 65 |
+
curl http://localhost:5000/api/health
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
### Test Summarization
|
| 69 |
+
```bash
|
| 70 |
+
curl -X POST http://localhost:5000/api/summarize \
|
| 71 |
+
-H "Content-Type: application/json" \
|
| 72 |
+
-d '{"text": "نص تجريبي للاختبار", "length": 2, "full_text": true}'
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
## 🐛 Troubleshooting
|
| 76 |
+
|
| 77 |
+
### Model Not Found
|
| 78 |
+
- Verify model path: `models/arabic_summarization_model/content/drive/MyDrive/arabic_summarization_model/`
|
| 79 |
+
- Check that `config.json` exists
|
| 80 |
+
- The app will search multiple possible locations automatically
|
| 81 |
+
|
| 82 |
+
### Dependencies Missing
|
| 83 |
+
```bash
|
| 84 |
+
python check_dependencies.py
|
| 85 |
+
pip install -r requirements.txt
|
| 86 |
+
```
|
| 87 |
+
|
| 88 |
+
### Port Already in Use
|
| 89 |
+
```bash
|
| 90 |
+
set PORT=5001
|
| 91 |
+
python run_app.py
|
| 92 |
+
```
|
| 93 |
+
|
| 94 |
+
## 📝 API Documentation
|
| 95 |
+
|
| 96 |
+
### POST /api/summarize
|
| 97 |
+
Summarize Arabic text.
|
| 98 |
+
|
| 99 |
+
**Request:**
|
| 100 |
+
```json
|
| 101 |
+
{
|
| 102 |
+
"text": "النص العربي...",
|
| 103 |
+
"length": 2, // 1=short, 2=medium, 3=long
|
| 104 |
+
"full_text": true
|
| 105 |
+
}
|
| 106 |
+
```
|
| 107 |
+
|
| 108 |
+
**Response:**
|
| 109 |
+
```json
|
| 110 |
+
{
|
| 111 |
+
"status": "success",
|
| 112 |
+
"summary": "الملخص...",
|
| 113 |
+
"original_length": 500,
|
| 114 |
+
"summary_length": 150
|
| 115 |
+
}
|
| 116 |
+
```
|
| 117 |
+
|
| 118 |
+
## 🎯 Next Steps
|
| 119 |
+
|
| 120 |
+
1. Install dependencies: `pip install -r requirements.txt`
|
| 121 |
+
2. Run the app: `python run_app.py`
|
| 122 |
+
3. Open browser: http://localhost:5000
|
| 123 |
+
4. Write Arabic text and click "توليد الملخص"
|
| 124 |
+
|
| 125 |
+
For detailed information, see `README_SETUP.md`.
|
| 126 |
+
|
README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Bayan API
|
| 3 |
+
emoji: ✍️
|
| 4 |
+
colorFrom: green
|
| 5 |
+
colorTo: blue
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
# Arabic Grammarly (project)
|
| 11 |
+
|
| 12 |
+
Project overview
|
| 13 |
+
|
| 14 |
+
This repository contains the initial skeleton for an Arabic grammar & writing assistant (like Grammarly) project. It includes placeholders for data, models, and source code, plus configuration and dependency files. Use this repo as the single source-of-truth for team contributions.
|
| 15 |
+
|
| 16 |
+
Key features you may implement here
|
| 17 |
+
|
| 18 |
+
- Arabic spelling and grammar checking
|
| 19 |
+
- Style / clarity suggestions for Modern Standard Arabic (MSA) and dialects
|
| 20 |
+
- Sentence rewriting and paraphrasing suggestions
|
| 21 |
+
- Plagiarism or similarity checks (optional)
|
| 22 |
+
- Integration with web UI / browser extension / API
|
| 23 |
+
|
| 24 |
+
Repository layout
|
| 25 |
+
|
| 26 |
+
- `data/` — place raw and processed datasets (LANS, corpora, etc.). See `data/README.md` for details.
|
| 27 |
+
- `models/` — store training checkpoints and exports (do NOT commit large binary files). See `models/README.md`.
|
| 28 |
+
- `src/` — source code (training scripts, inference API, preprocessing). See `src/README.md`.
|
| 29 |
+
- `requirements.txt` — Python dependencies for the project.
|
| 30 |
+
- `.env.example` — template for environment variables.
|
| 31 |
+
- `.gitignore` — sensible defaults for this project.
|
| 32 |
+
|
| 33 |
+
Contributing notes
|
| 34 |
+
|
| 35 |
+
- Keep large datasets and model weights out of Git (use cloud storage or Git LFS / DVC / Hugging Face Hub).
|
| 36 |
+
- Add tests in `src/tests/` and keep the public API stable.
|
| 37 |
+
- Use small, focused pull requests that include a short description and test(s) if applicable.
|
| 38 |
+
|
| 39 |
+
Contact
|
| 40 |
+
|
| 41 |
+
If you have questions about where to add files or how to name things, ask in the team chat and follow the README inside each folder for more guidance.
|
README_HF.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Bayan API
|
| 3 |
+
emoji: ✍️
|
| 4 |
+
colorFrom: green
|
| 5 |
+
colorTo: blue
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
---
|
README_SETUP.md
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Bayan - Arabic Text Summarization Setup Guide
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
Bayan is an Arabic text summarization application with a web interface. This guide will help you set up and run the application.
|
| 5 |
+
|
| 6 |
+
## Prerequisites
|
| 7 |
+
- Python 3.8 or higher
|
| 8 |
+
- pip (Python package manager)
|
| 9 |
+
- At least 4GB RAM (8GB+ recommended for better performance)
|
| 10 |
+
- Model files in the correct location (see below)
|
| 11 |
+
|
| 12 |
+
## Installation Steps
|
| 13 |
+
|
| 14 |
+
### 1. Install Dependencies
|
| 15 |
+
```bash
|
| 16 |
+
pip install -r requirements.txt
|
| 17 |
+
```
|
| 18 |
+
|
| 19 |
+
**Note:** If you encounter issues installing PyTorch, you may need to install it separately:
|
| 20 |
+
- For CPU: `pip install torch --index-url https://download.pytorch.org/whl/cpu`
|
| 21 |
+
- For CUDA: Visit https://pytorch.org/get-started/locally/ for the appropriate command
|
| 22 |
+
|
| 23 |
+
### 2. Verify Model Location
|
| 24 |
+
The model should be located at:
|
| 25 |
+
```
|
| 26 |
+
models/arabic_summarization_model/content/drive/MyDrive/arabic_summarization_model/
|
| 27 |
+
```
|
| 28 |
+
|
| 29 |
+
Required files:
|
| 30 |
+
- `config.json`
|
| 31 |
+
- `tokenizer.json`
|
| 32 |
+
- `model.safetensors`
|
| 33 |
+
- `sentencepiece.bpe.model`
|
| 34 |
+
- Other tokenizer/model files
|
| 35 |
+
|
| 36 |
+
### 3. Run the Application
|
| 37 |
+
|
| 38 |
+
#### Option A: Using the run script (Recommended)
|
| 39 |
+
```bash
|
| 40 |
+
python run_app.py
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
#### Option B: Direct Flask run
|
| 44 |
+
```bash
|
| 45 |
+
cd src
|
| 46 |
+
python app.py
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
#### Option C: Using Flask CLI
|
| 50 |
+
```bash
|
| 51 |
+
cd src
|
| 52 |
+
export FLASK_APP=app.py
|
| 53 |
+
flask run
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
### 4. Access the Application
|
| 57 |
+
Open your browser and navigate to:
|
| 58 |
+
```
|
| 59 |
+
http://localhost:5000
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
## Configuration
|
| 63 |
+
|
| 64 |
+
### Environment Variables
|
| 65 |
+
- `PORT`: Server port (default: 5000)
|
| 66 |
+
- `DEBUG`: Enable debug mode (default: False)
|
| 67 |
+
```bash
|
| 68 |
+
export DEBUG=True
|
| 69 |
+
export PORT=8080
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
### Supabase Authentication (Phase 5)
|
| 73 |
+
|
| 74 |
+
See `.env.example` and `PHASE_5_IMPLEMENTATION_PLAN.md`.
|
| 75 |
+
|
| 76 |
+
1. Create a Supabase project and enable **Anonymous** + **Google** auth.
|
| 77 |
+
2. Run `supabase/migrations/001_profiles.sql` in the SQL Editor.
|
| 78 |
+
3. Set meta tags in `src/index.html`:
|
| 79 |
+
```html
|
| 80 |
+
<meta name="supabase-url" content="https://YOUR_PROJECT.supabase.co">
|
| 81 |
+
<meta name="supabase-anon-key" content="YOUR_ANON_KEY">
|
| 82 |
+
```
|
| 83 |
+
4. Add redirect URL: `http://localhost:5000/**`
|
| 84 |
+
|
| 85 |
+
If Supabase is not configured, the editor still works in offline auth mode.
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
### Model Not Found Error
|
| 89 |
+
If you see "Model not found" error:
|
| 90 |
+
1. Verify the model path exists
|
| 91 |
+
2. Check that all required files are present
|
| 92 |
+
3. The application will search multiple possible paths automatically
|
| 93 |
+
|
| 94 |
+
### Out of Memory Error
|
| 95 |
+
If you encounter memory issues:
|
| 96 |
+
1. Close other applications
|
| 97 |
+
2. Use CPU mode (it will automatically use CPU if CUDA is not available)
|
| 98 |
+
3. Reduce the `MAX_TEXT_LENGTH` in `src/app.py` if needed
|
| 99 |
+
|
| 100 |
+
### Port Already in Use
|
| 101 |
+
If port 5000 is already in use:
|
| 102 |
+
```bash
|
| 103 |
+
export PORT=5001
|
| 104 |
+
python run_app.py
|
| 105 |
+
```
|
| 106 |
+
|
| 107 |
+
### Slow Performance
|
| 108 |
+
- First run will be slower as the model loads
|
| 109 |
+
- Subsequent requests will be faster
|
| 110 |
+
- Using GPU (CUDA) significantly improves performance
|
| 111 |
+
|
| 112 |
+
## API Endpoints
|
| 113 |
+
|
| 114 |
+
### Health Check
|
| 115 |
+
```
|
| 116 |
+
GET /api/health
|
| 117 |
+
```
|
| 118 |
+
Returns server status and model loading state.
|
| 119 |
+
|
| 120 |
+
### Summarize Text
|
| 121 |
+
```
|
| 122 |
+
POST /api/summarize
|
| 123 |
+
Content-Type: application/json
|
| 124 |
+
|
| 125 |
+
{
|
| 126 |
+
"text": "النص العربي المراد تلخيصه...",
|
| 127 |
+
"length": 2, // 1=short, 2=medium, 3=long
|
| 128 |
+
"full_text": true
|
| 129 |
+
}
|
| 130 |
+
```
|
| 131 |
+
|
| 132 |
+
Response:
|
| 133 |
+
```json
|
| 134 |
+
{
|
| 135 |
+
"status": "success",
|
| 136 |
+
"summary": "الملخص المولد...",
|
| 137 |
+
"original_length": 500,
|
| 138 |
+
"summary_length": 150
|
| 139 |
+
}
|
| 140 |
+
```
|
| 141 |
+
|
| 142 |
+
## Security Features
|
| 143 |
+
|
| 144 |
+
- Input validation (text length limits)
|
| 145 |
+
- CORS enabled for web interface
|
| 146 |
+
- Error handling and logging
|
| 147 |
+
- Path validation for model files
|
| 148 |
+
- Safe model loading with fallbacks
|
| 149 |
+
|
| 150 |
+
## Development
|
| 151 |
+
|
| 152 |
+
### Running in Debug Mode
|
| 153 |
+
```bash
|
| 154 |
+
export DEBUG=True
|
| 155 |
+
python run_app.py
|
| 156 |
+
```
|
| 157 |
+
|
| 158 |
+
### Testing the API
|
| 159 |
+
```bash
|
| 160 |
+
curl -X POST http://localhost:5000/api/summarize \
|
| 161 |
+
-H "Content-Type: application/json" \
|
| 162 |
+
-d '{"text": "نص تجريبي للاختبار", "length": 2, "full_text": true}'
|
| 163 |
+
```
|
| 164 |
+
|
| 165 |
+
## Support
|
| 166 |
+
|
| 167 |
+
For issues or questions:
|
| 168 |
+
1. Check the logs in the terminal
|
| 169 |
+
2. Verify model files are correct
|
| 170 |
+
3. Ensure all dependencies are installed
|
| 171 |
+
4. Check Python version compatibility
|
| 172 |
+
|
READMEquran.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Bayan Search API
|
| 2 |
+
|
| 3 |
+
## Supported Languages
|
| 4 |
+
|
| 5 |
+
The `search_bayan()` function supports the following values for the `target_type` parameter:
|
| 6 |
+
|
| 7 |
+
```python
|
| 8 |
+
languages = [
|
| 9 |
+
"تدقيق الايات",
|
| 10 |
+
"english",
|
| 11 |
+
"french",
|
| 12 |
+
"turkish",
|
| 13 |
+
"persian",
|
| 14 |
+
"urdu",
|
| 15 |
+
"russian",
|
| 16 |
+
"spanish",
|
| 17 |
+
"german",
|
| 18 |
+
"indonesian",
|
| 19 |
+
"malay",
|
| 20 |
+
"bengali",
|
| 21 |
+
"bosnian",
|
| 22 |
+
"portuguese",
|
| 23 |
+
"uzbek"
|
| 24 |
+
]
|
| 25 |
+
```
|
| 26 |
+
|
| 27 |
+
## Language Descriptions
|
| 28 |
+
|
| 29 |
+
| Value | Output |
|
| 30 |
+
| ------------ | ------------------------------------- |
|
| 31 |
+
| تدقيق الايات | Quran text in Uthmani script (Arabic) |
|
| 32 |
+
| english | English translation |
|
| 33 |
+
| french | French translation |
|
| 34 |
+
| turkish | Turkish translation |
|
| 35 |
+
| persian | Persian (Farsi) translation |
|
| 36 |
+
| urdu | Urdu translation |
|
| 37 |
+
| russian | Russian translation |
|
| 38 |
+
| spanish | Spanish translation |
|
| 39 |
+
| german | German translation |
|
| 40 |
+
| indonesian | Indonesian translation |
|
| 41 |
+
| malay | Malay translation |
|
| 42 |
+
| bengali | Bengali translation |
|
| 43 |
+
| bosnian | Bosnian translation |
|
| 44 |
+
| portuguese | Portuguese translation |
|
| 45 |
+
| uzbek | Uzbek translation |
|
| 46 |
+
|
| 47 |
+
## Example Usage
|
| 48 |
+
|
| 49 |
+
```python
|
| 50 |
+
result = search_bayan(
|
| 51 |
+
"ولله المشرق والمغرب فأينما تولوا فثم وجه الله",
|
| 52 |
+
target_type="english"
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
print(result["matched_segment"])
|
| 56 |
+
print(result["full_verse"])
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
## Notes
|
| 60 |
+
|
| 61 |
+
* If `target_type` is omitted, the default value is `"تدقيق الايات"`.
|
| 62 |
+
* The search engine supports fuzzy matching and can handle minor spelling mistakes.
|
| 63 |
+
* Quranic Uthmani text is returned when using `"تدقيق الايات"`.
|
| 64 |
+
* Translations are returned when using any of the supported language names above.
|
add_divider.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
|
| 3 |
+
with open('src/index.html', 'r', encoding='utf-8') as f:
|
| 4 |
+
html = f.read()
|
| 5 |
+
|
| 6 |
+
# Replace Navbar
|
| 7 |
+
navbar_pattern = r'(<button onclick="showPage\(\'home\'\)" class="flex items-center) gap-3(" style="background:none;border:none;cursor:pointer;" aria-label="الرئيسية">)(.*?)(<span id="nav-brand" class="text-xl md:text-2xl font-bold text-gradient">بيان</span></button>)'
|
| 8 |
+
navbar_replacement = r'\1 gap-2.5 md:gap-3\2\3<div class="h-6 w-[1.5px] bg-gray-300 dark:bg-gray-700 rounded-full"></div>\4'
|
| 9 |
+
html = re.sub(navbar_pattern, navbar_replacement, html, flags=re.DOTALL)
|
| 10 |
+
|
| 11 |
+
# Replace Footer
|
| 12 |
+
footer_pattern = r'(<div class="flex items-center) gap-3( mb-4">)(.*?)(<span id="footer-brand" class="text-2xl font-bold text-gradient">بيان</span>)'
|
| 13 |
+
footer_replacement = r'\1 gap-2.5 md:gap-3\2\3<div class="h-7 w-[1.5px] bg-gray-300 dark:bg-gray-700 rounded-full"></div>\4'
|
| 14 |
+
html = re.sub(footer_pattern, footer_replacement, html, flags=re.DOTALL)
|
| 15 |
+
|
| 16 |
+
with open('src/index.html', 'w', encoding='utf-8') as f:
|
| 17 |
+
f.write(html)
|
| 18 |
+
|
| 19 |
+
print("Done replacing.")
|
add_extension_theme_toggle.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
|
| 3 |
+
# 1. CSS Injection
|
| 4 |
+
css_to_add = """
|
| 5 |
+
/* Light Theme Variables */
|
| 6 |
+
[data-theme="light"] {
|
| 7 |
+
--bayan-bg: #f9fafb;
|
| 8 |
+
--bayan-surface: #ffffff;
|
| 9 |
+
--bayan-surface-hover: #f3f4f6;
|
| 10 |
+
--bayan-surface-active: #e5e7eb;
|
| 11 |
+
--bayan-border: #e5e7eb;
|
| 12 |
+
--bayan-border-light: #d1d5db;
|
| 13 |
+
--bayan-text: #111827;
|
| 14 |
+
--bayan-text-secondary: #4b5563;
|
| 15 |
+
--bayan-text-muted: #9ca3af;
|
| 16 |
+
--bayan-success: #16a34a;
|
| 17 |
+
--bayan-warning: #d97706;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
/* Theme Toggle Button Styles */
|
| 21 |
+
.theme-toggle-animated {
|
| 22 |
+
display: flex;
|
| 23 |
+
align-items: center;
|
| 24 |
+
justify-content: center;
|
| 25 |
+
width: 32px;
|
| 26 |
+
height: 32px;
|
| 27 |
+
border: none;
|
| 28 |
+
border-radius: 50%;
|
| 29 |
+
background: var(--bayan-surface-hover);
|
| 30 |
+
color: var(--bayan-text-secondary);
|
| 31 |
+
cursor: pointer;
|
| 32 |
+
transition: background 0.3s ease, transform 0.3s ease, color 0.3s ease;
|
| 33 |
+
position: relative;
|
| 34 |
+
overflow: hidden;
|
| 35 |
+
margin-right: 8px;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
.theme-toggle-animated:hover {
|
| 39 |
+
background: var(--bayan-primary);
|
| 40 |
+
color: #fff;
|
| 41 |
+
transform: rotate(15deg);
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
.theme-toggle-animated svg {
|
| 45 |
+
transition: transform 0.4s ease, opacity 0.3s ease;
|
| 46 |
+
position: absolute;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
[data-theme="dark"] .theme-icon-sun {
|
| 50 |
+
transform: rotate(90deg) scale(0);
|
| 51 |
+
opacity: 0;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
[data-theme="dark"] .theme-icon-moon {
|
| 55 |
+
transform: rotate(0) scale(1);
|
| 56 |
+
opacity: 1;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
[data-theme="light"] .theme-icon-moon {
|
| 60 |
+
transform: rotate(-90deg) scale(0);
|
| 61 |
+
opacity: 0;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
[data-theme="light"] .theme-icon-sun {
|
| 65 |
+
transform: rotate(0) scale(1);
|
| 66 |
+
opacity: 1;
|
| 67 |
+
}
|
| 68 |
+
"""
|
| 69 |
+
|
| 70 |
+
def append_to_file(filepath, content):
|
| 71 |
+
with open(filepath, 'a', encoding='utf-8') as f:
|
| 72 |
+
f.write('\n' + content + '\n')
|
| 73 |
+
|
| 74 |
+
append_to_file('extension/popup.css', css_to_add)
|
| 75 |
+
append_to_file('extension/sidepanel/sidepanel.css', css_to_add)
|
| 76 |
+
|
| 77 |
+
# 2. HTML Injection
|
| 78 |
+
btn_html = """
|
| 79 |
+
<button id="ext-theme-toggle" class="theme-toggle-animated" aria-label="تبديل السمة" type="button">
|
| 80 |
+
<svg class="theme-icon-sun" width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"/></svg>
|
| 81 |
+
<svg class="theme-icon-moon" width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"/></svg>
|
| 82 |
+
</button>
|
| 83 |
+
"""
|
| 84 |
+
|
| 85 |
+
def insert_html_button(filepath, pattern):
|
| 86 |
+
with open(filepath, 'r', encoding='utf-8') as f:
|
| 87 |
+
html = f.read()
|
| 88 |
+
|
| 89 |
+
# We want to put the button next to the status indicator.
|
| 90 |
+
# The pattern will match the <div class="bayan-header-status"...> (or sp-) and inject the button right before it
|
| 91 |
+
new_html = re.sub(pattern, btn_html + r'\1', html)
|
| 92 |
+
with open(filepath, 'w', encoding='utf-8') as f:
|
| 93 |
+
f.write(new_html)
|
| 94 |
+
|
| 95 |
+
insert_html_button('extension/popup.html', r'(<div class="bayan-header-status")')
|
| 96 |
+
insert_html_button('extension/sidepanel/sidepanel.html', r'(<div class="sp-header-status")')
|
| 97 |
+
|
| 98 |
+
# 3. JS Logic Injection
|
| 99 |
+
js_to_add = """
|
| 100 |
+
// ── Theme Toggle Logic ──
|
| 101 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 102 |
+
const toggleBtn = document.getElementById('ext-theme-toggle');
|
| 103 |
+
|
| 104 |
+
// Load theme from storage
|
| 105 |
+
chrome.storage.local.get(['theme'], (result) => {
|
| 106 |
+
const currentTheme = result.theme || 'dark'; // default to dark
|
| 107 |
+
document.documentElement.setAttribute('data-theme', currentTheme);
|
| 108 |
+
});
|
| 109 |
+
|
| 110 |
+
if (toggleBtn) {
|
| 111 |
+
toggleBtn.addEventListener('click', () => {
|
| 112 |
+
let theme = document.documentElement.getAttribute('data-theme') || 'dark';
|
| 113 |
+
let targetTheme = theme === 'dark' ? 'light' : 'dark';
|
| 114 |
+
document.documentElement.setAttribute('data-theme', targetTheme);
|
| 115 |
+
chrome.storage.local.set({ theme: targetTheme });
|
| 116 |
+
});
|
| 117 |
+
}
|
| 118 |
+
});
|
| 119 |
+
"""
|
| 120 |
+
|
| 121 |
+
append_to_file('extension/popup.js', js_to_add)
|
| 122 |
+
append_to_file('extension/sidepanel/sidepanel.js', js_to_add)
|
| 123 |
+
|
| 124 |
+
print("Theme toggle added successfully.")
|
analyze_failures.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
|
| 3 |
+
with open('tests/phase10/reports/phase10_results.json', 'r', encoding='utf-8') as f:
|
| 4 |
+
data = json.load(f)
|
| 5 |
+
|
| 6 |
+
failures = [r for r in data['results'] if r['pipeline_verdict'] in ('FP', 'FN', 'ERROR')]
|
| 7 |
+
|
| 8 |
+
md_content = "# Analysis of the 33 Benchmark Failures\n\n"
|
| 9 |
+
md_content += "This document contains a detailed breakdown of the 33 examples that failed the benchmark, grouped by their dataset.\n\n"
|
| 10 |
+
|
| 11 |
+
from collections import defaultdict
|
| 12 |
+
grouped = defaultdict(list)
|
| 13 |
+
for r in failures:
|
| 14 |
+
grouped[r.get('dataset', 'unknown')].append(r)
|
| 15 |
+
|
| 16 |
+
for dataset, items in grouped.items():
|
| 17 |
+
md_content += f"## Dataset: {dataset.upper()} ({len(items)} failures)\n\n"
|
| 18 |
+
for idx, item in enumerate(items, 1):
|
| 19 |
+
md_content += f"### {idx}. ID: {item.get('id')} ({item.get('pipeline_verdict')})\n"
|
| 20 |
+
md_content += f"- **Input:** `{item.get('input')}`\n"
|
| 21 |
+
md_content += f"- **Expected:** `{item.get('expected')}`\n"
|
| 22 |
+
md_content += f"- **Actual Output:** `{item.get('pipeline_output')}`\n"
|
| 23 |
+
md_content += f"- **Failure Reason:** {item.get('pipeline_detail', 'N/A')}\n"
|
| 24 |
+
md_content += f"- **Root Cause:** {item.get('root_cause_stage', 'unknown')} ({item.get('root_cause_detail', 'N/A')})\n"
|
| 25 |
+
|
| 26 |
+
md_content += "\n"
|
| 27 |
+
|
| 28 |
+
with open('C:\\Users\\youss\\.gemini\\antigravity-ide\\brain\\9f7cefbc-f722-4b96-bc24-80ce6ffbd124\\failures_analysis.md', 'w', encoding='utf-8') as out:
|
| 29 |
+
out.write(md_content)
|
| 30 |
+
|
| 31 |
+
print("Analysis successfully written to artifact.")
|
apply_locks.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
|
| 3 |
+
def apply_lock_to_file(filepath, var_name, engine_name, func_name):
|
| 4 |
+
with open(filepath, 'r', encoding='utf-8') as f:
|
| 5 |
+
lines = f.readlines()
|
| 6 |
+
|
| 7 |
+
out_lines = []
|
| 8 |
+
in_imports = False
|
| 9 |
+
added_threading = False
|
| 10 |
+
in_globals = False
|
| 11 |
+
added_lock_var = False
|
| 12 |
+
in_func = False
|
| 13 |
+
|
| 14 |
+
for line in lines:
|
| 15 |
+
if line.startswith('import ') and not added_threading:
|
| 16 |
+
out_lines.append(line)
|
| 17 |
+
out_lines.append("import threading\n")
|
| 18 |
+
added_threading = True
|
| 19 |
+
continue
|
| 20 |
+
|
| 21 |
+
if line.startswith(f'_{var_name} = None') and not added_lock_var:
|
| 22 |
+
out_lines.append(line)
|
| 23 |
+
out_lines.append(f"_load_lock = threading.Lock()\n")
|
| 24 |
+
added_lock_var = True
|
| 25 |
+
continue
|
| 26 |
+
|
| 27 |
+
if line.startswith(f'def {func_name}('):
|
| 28 |
+
in_func = True
|
| 29 |
+
out_lines.append(line)
|
| 30 |
+
continue
|
| 31 |
+
|
| 32 |
+
if in_func:
|
| 33 |
+
if line.startswith(f' global '):
|
| 34 |
+
out_lines.append(line.replace('\n', f', _load_lock\n'))
|
| 35 |
+
continue
|
| 36 |
+
|
| 37 |
+
if line.startswith(f' try:'):
|
| 38 |
+
# The start of the old try block. We wrap everything from here.
|
| 39 |
+
out_lines.append(f' with _load_lock:\n')
|
| 40 |
+
out_lines.append(f' if _{var_name} is not None:\n')
|
| 41 |
+
out_lines.append(f' return _{var_name}\n\n')
|
| 42 |
+
out_lines.append(f' try:\n')
|
| 43 |
+
continue
|
| 44 |
+
|
| 45 |
+
# If we are inside the function and past the global declaration,
|
| 46 |
+
# and it's indented with at least 4 spaces, we need to add 4 more spaces
|
| 47 |
+
# for the lines that were inside the old `try:` and `except:`
|
| 48 |
+
# EXCEPT for `if _xxx is not None: return _xxx` which comes before the try
|
| 49 |
+
if line.startswith(' if _') or line.startswith(' return _'):
|
| 50 |
+
# This is the old `if checker is not None:` logic before try. Leave it alone.
|
| 51 |
+
out_lines.append(line)
|
| 52 |
+
continue
|
| 53 |
+
|
| 54 |
+
if line.startswith(' '):
|
| 55 |
+
# Shift everything that was inside try/except right by 4 spaces
|
| 56 |
+
if line.strip() == '':
|
| 57 |
+
out_lines.append('\n')
|
| 58 |
+
else:
|
| 59 |
+
out_lines.append(' ' + line)
|
| 60 |
+
|
| 61 |
+
if line.startswith(' return _') or line.startswith(' raise RuntimeError'):
|
| 62 |
+
# End of function
|
| 63 |
+
in_func = False
|
| 64 |
+
continue
|
| 65 |
+
|
| 66 |
+
out_lines.append(line)
|
| 67 |
+
|
| 68 |
+
with open(filepath, 'w', encoding='utf-8') as f:
|
| 69 |
+
f.writelines(out_lines)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
apply_lock_to_file(r'src/nlp/spelling/araspell_service.py', 'spell_checker', 'AraSpell', 'get_spelling_model')
|
| 73 |
+
apply_lock_to_file(r'src/nlp/punctuation/punctuation_service.py', 'punctuation_checker', 'PuncAra', 'get_punctuation_model')
|
| 74 |
+
apply_lock_to_file(r'src/nlp/grammar/grammar_service.py', 'grammar_checker', 'Grammar', 'get_grammar_model')
|
| 75 |
+
apply_lock_to_file(r'src/nlp/autocomplete/autocomplete_service.py', 'autocomplete_engine', 'Autocomplete', 'get_autocomplete_model')
|
| 76 |
+
|
| 77 |
+
print("Locks applied perfectly with correct indentation!")
|
archive/legacy_scripts/AraSpell.py
ADDED
|
@@ -0,0 +1,2224 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# AraSpell — Arabic Spell Checker Pipeline
|
| 2 |
+
# Production-ready version
|
| 3 |
+
|
| 4 |
+
import re
|
| 5 |
+
import math
|
| 6 |
+
import logging
|
| 7 |
+
import torch
|
| 8 |
+
import os
|
| 9 |
+
from collections import Counter
|
| 10 |
+
from transformers import AutoTokenizer, EncoderDecoderModel
|
| 11 |
+
import Levenshtein
|
| 12 |
+
import jellyfish
|
| 13 |
+
|
| 14 |
+
logger = logging.getLogger(__name__)
|
| 15 |
+
logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
|
| 16 |
+
|
| 17 |
+
# ═══════════════════════════════════════════════════════════════════════════════
|
| 18 |
+
# LOAD ARABERT SEQ2SEQ MODEL
|
| 19 |
+
# ═══════════════════════════════════════════════════════════════════════════════
|
| 20 |
+
|
| 21 |
+
from huggingface_hub import hf_hub_download
|
| 22 |
+
|
| 23 |
+
MODEL_REPO = 'bayan10/AraSpell-Model'
|
| 24 |
+
MODEL_FILENAME = 'last_model.pt'
|
| 25 |
+
|
| 26 |
+
try:
|
| 27 |
+
logger.info(f"Downloading/loading model from Hugging Face: {MODEL_REPO}")
|
| 28 |
+
MODEL_PATH = hf_hub_download(repo_id=MODEL_REPO, filename=MODEL_FILENAME)
|
| 29 |
+
except Exception as e:
|
| 30 |
+
raise RuntimeError(f"Failed to download model from Hugging Face: {e}")
|
| 31 |
+
|
| 32 |
+
MODEL_NAME = 'aubmindlab/bert-base-arabertv02'
|
| 33 |
+
|
| 34 |
+
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
|
| 35 |
+
model = EncoderDecoderModel.from_encoder_decoder_pretrained(MODEL_NAME, MODEL_NAME)
|
| 36 |
+
|
| 37 |
+
model.config.decoder_start_token_id = tokenizer.cls_token_id
|
| 38 |
+
model.config.pad_token_id = tokenizer.pad_token_id
|
| 39 |
+
model.config.eos_token_id = tokenizer.sep_token_id
|
| 40 |
+
model.generation_config.max_length = 128
|
| 41 |
+
model.generation_config.decoder_start_token_id = tokenizer.cls_token_id
|
| 42 |
+
model.generation_config.pad_token_id = tokenizer.pad_token_id
|
| 43 |
+
model.generation_config.eos_token_id = tokenizer.sep_token_id
|
| 44 |
+
|
| 45 |
+
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
| 46 |
+
checkpoint = torch.load(MODEL_PATH, map_location=device, weights_only=False)
|
| 47 |
+
model.load_state_dict(checkpoint['model_state_dict'], strict=False)
|
| 48 |
+
model = model.to(device)
|
| 49 |
+
model.eval()
|
| 50 |
+
|
| 51 |
+
logger.info(f"Model loaded on {device}, epoch: {checkpoint.get('epoch', 'N/A')}")
|
| 52 |
+
|
| 53 |
+
from enum import Enum
|
| 54 |
+
from typing import List, Tuple, Optional
|
| 55 |
+
|
| 56 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 57 |
+
# ERROR TYPE ENUM
|
| 58 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 59 |
+
|
| 60 |
+
class ErrorType(Enum):
|
| 61 |
+
"""Types of spelling errors"""
|
| 62 |
+
CHAR_REPETITION = "char_repetition"
|
| 63 |
+
WORD_MERGE = "word_merge"
|
| 64 |
+
CHAR_SUBSTITUTION = "char_substitution"
|
| 65 |
+
MIXED = "mixed"
|
| 66 |
+
CLEAN = "clean"
|
| 67 |
+
|
| 68 |
+
# ═══════════════════════════════════════════════════════════════════════════════
|
| 69 |
+
# POST PROCESSOR
|
| 70 |
+
# ═══════════════════════════════════════════════════════════════════════════════
|
| 71 |
+
|
| 72 |
+
class AraSpellPostProcessor:
|
| 73 |
+
"""Arabic text post-processing techniques."""
|
| 74 |
+
|
| 75 |
+
ARABIC_HARAKAT = 'ًٌٍَُِّْ'
|
| 76 |
+
TATWEEL = 'ـ'
|
| 77 |
+
NORMALIZER_MAP = {
|
| 78 |
+
'ﻹ': 'لإ', 'ﻷ': 'لأ', 'ﻵ': 'لآ', 'ﻻ': 'لا', 'ﷲ': 'الله'
|
| 79 |
+
}
|
| 80 |
+
ARABIC_CONSONANTS = set('بتثجحخدذرزسشصضطظعغفقكلمن')
|
| 81 |
+
|
| 82 |
+
# --- Basic Normalization ---
|
| 83 |
+
|
| 84 |
+
@staticmethod
|
| 85 |
+
def remove_harakat(text: str) -> str:
|
| 86 |
+
"""Remove Arabic diacritics"""
|
| 87 |
+
return re.sub(r'[ً-ْ]', '', text)
|
| 88 |
+
|
| 89 |
+
@staticmethod
|
| 90 |
+
def remove_tatweel(text: str) -> str:
|
| 91 |
+
"""Remove Arabic kashida/tatweel"""
|
| 92 |
+
return text.replace(AraSpellPostProcessor.TATWEEL, '')
|
| 93 |
+
|
| 94 |
+
@staticmethod
|
| 95 |
+
def normalize_special_chars(text: str) -> str:
|
| 96 |
+
"""Normalize special Arabic ligatures"""
|
| 97 |
+
for old, new in AraSpellPostProcessor.NORMALIZER_MAP.items():
|
| 98 |
+
text = text.replace(old, new)
|
| 99 |
+
return text
|
| 100 |
+
|
| 101 |
+
# --- Core Functions ---
|
| 102 |
+
|
| 103 |
+
@staticmethod
|
| 104 |
+
def unified_collapse_repeated(text: str) -> str:
|
| 105 |
+
"""
|
| 106 |
+
Collapse repeated characters.
|
| 107 |
+
Arabic: 3+ consecutive → 1 | Latin: 2+ consecutive → 1
|
| 108 |
+
"""
|
| 109 |
+
# Arabic characters: 3+ → 1
|
| 110 |
+
text = re.sub(r"([\u0600-\u06FF])\1{2,}", r"\1", text)
|
| 111 |
+
|
| 112 |
+
# Latin characters: 2+ → 1
|
| 113 |
+
text = re.sub(r"([a-zA-Z])\1+", r"\1", text)
|
| 114 |
+
|
| 115 |
+
return text
|
| 116 |
+
|
| 117 |
+
@staticmethod
|
| 118 |
+
def remove_duplicate_words(text: str) -> str:
|
| 119 |
+
"""Remove consecutive duplicate words. e.g. كتاب كتاب → كتاب"""
|
| 120 |
+
words = text.split()
|
| 121 |
+
if len(words) < 2:
|
| 122 |
+
return text
|
| 123 |
+
|
| 124 |
+
result = [words[0]]
|
| 125 |
+
for i in range(1, len(words)):
|
| 126 |
+
if words[i] != words[i-1]:
|
| 127 |
+
result.append(words[i])
|
| 128 |
+
|
| 129 |
+
return ' '.join(result)
|
| 130 |
+
|
| 131 |
+
@staticmethod
|
| 132 |
+
def normalize_spaces(text: str) -> str:
|
| 133 |
+
"""Normalize whitespace: multiple spaces, unicode spaces, punctuation spacing."""
|
| 134 |
+
# Multiple spaces → single
|
| 135 |
+
text = re.sub(r' +', ' ', text)
|
| 136 |
+
|
| 137 |
+
# Unicode spaces
|
| 138 |
+
text = text.replace('\u00A0', ' ') # Non-breaking space
|
| 139 |
+
text = text.replace('\u200B', '') # Zero-width space
|
| 140 |
+
text = text.replace('\u200C', '') # Zero-width non-joiner
|
| 141 |
+
text = text.replace('\u200D', '') # Zero-width joiner
|
| 142 |
+
|
| 143 |
+
# Trim
|
| 144 |
+
text = text.strip()
|
| 145 |
+
|
| 146 |
+
# Punctuation spacing
|
| 147 |
+
text = re.sub(r'\s*([،؛؟!.])\s*', r'\1 ', text)
|
| 148 |
+
text = text.strip()
|
| 149 |
+
|
| 150 |
+
return text
|
| 151 |
+
|
| 152 |
+
@staticmethod
|
| 153 |
+
def remove_word_repetition_with_wa(text: str) -> str:
|
| 154 |
+
"""Remove word و word → word"""
|
| 155 |
+
words = text.split()
|
| 156 |
+
result = []
|
| 157 |
+
i = 0
|
| 158 |
+
while i < len(words):
|
| 159 |
+
if i + 2 < len(words) and words[i] == words[i+2] and words[i+1] == 'و':
|
| 160 |
+
result.append(words[i])
|
| 161 |
+
i += 3
|
| 162 |
+
else:
|
| 163 |
+
result.append(words[i])
|
| 164 |
+
i += 1
|
| 165 |
+
return ' '.join(result)
|
| 166 |
+
|
| 167 |
+
# --- Hamza & Ta Marbuta Handling ---
|
| 168 |
+
|
| 169 |
+
@staticmethod
|
| 170 |
+
def fix_hamza_conservative(text: str) -> str:
|
| 171 |
+
"""Conservative Hamza normalization — only at word END, not middle."""
|
| 172 |
+
words = text.split()
|
| 173 |
+
result = []
|
| 174 |
+
|
| 175 |
+
for word in words:
|
| 176 |
+
if len(word) >= 3:
|
| 177 |
+
# Fix trailing أ → ا
|
| 178 |
+
if word.endswith('أ'):
|
| 179 |
+
word = word[:-1] + 'ا'
|
| 180 |
+
|
| 181 |
+
# Fix trailing إ → ا
|
| 182 |
+
if word.endswith('إ'):
|
| 183 |
+
word = word[:-1] + 'ا'
|
| 184 |
+
|
| 185 |
+
result.append(word)
|
| 186 |
+
|
| 187 |
+
return ' '.join(result)
|
| 188 |
+
|
| 189 |
+
@staticmethod
|
| 190 |
+
def fix_ha_ta_marbuta(text: str, vocab_manager=None) -> str:
|
| 191 |
+
"""
|
| 192 |
+
Smart ه → ة fix at end of words.
|
| 193 |
+
|
| 194 |
+
Key insight: ه at word end can be:
|
| 195 |
+
- Ta Marbuta (should be ة): المدرسه → المدرسة
|
| 196 |
+
- Possessive pronoun (should stay ه): تحقيقه = his achievement
|
| 197 |
+
|
| 198 |
+
Strategy: Only convert if the ة version is IV (in tokenizer vocab).
|
| 199 |
+
This distinguishes المدرسة (IV) from تحقيقة (not a real word form).
|
| 200 |
+
Without vocab_manager, falls back to original pattern-based approach.
|
| 201 |
+
"""
|
| 202 |
+
# Protected words: anything containing لله
|
| 203 |
+
PROTECTED_ENDINGS = ['لله']
|
| 204 |
+
|
| 205 |
+
words = text.split()
|
| 206 |
+
result = []
|
| 207 |
+
|
| 208 |
+
for word in words:
|
| 209 |
+
# Skip protected words (Allah-related)
|
| 210 |
+
if any(word.endswith(e) for e in PROTECTED_ENDINGS):
|
| 211 |
+
result.append(word)
|
| 212 |
+
continue
|
| 213 |
+
|
| 214 |
+
if len(word) >= 4 and word.endswith('ه'):
|
| 215 |
+
# Check if second-to-last char is a consonant
|
| 216 |
+
if word[-2] in AraSpellPostProcessor.ARABIC_CONSONANTS:
|
| 217 |
+
candidate_with_ta = word[:-1] + 'ة'
|
| 218 |
+
|
| 219 |
+
if vocab_manager:
|
| 220 |
+
# SMART MODE: Use vocab to decide
|
| 221 |
+
ta_iv = vocab_manager.is_iv(candidate_with_ta)
|
| 222 |
+
ha_iv = vocab_manager.is_iv(word)
|
| 223 |
+
|
| 224 |
+
if ta_iv:
|
| 225 |
+
# ة version is IV → convert (المدرسه→المدرسة)
|
| 226 |
+
result.append(candidate_with_ta)
|
| 227 |
+
continue
|
| 228 |
+
elif ha_iv:
|
| 229 |
+
# Only ه version is IV → keep ه (possessive: تحقيقه)
|
| 230 |
+
result.append(word)
|
| 231 |
+
continue
|
| 232 |
+
# else: NEITHER is IV → keep original ه
|
| 233 |
+
# (safer than guessing — could be rare possessive)
|
| 234 |
+
else:
|
| 235 |
+
# FALLBACK: No vocab → use original pattern-based approach
|
| 236 |
+
result.append(candidate_with_ta)
|
| 237 |
+
continue
|
| 238 |
+
result.append(word)
|
| 239 |
+
|
| 240 |
+
return ' '.join(result)
|
| 241 |
+
|
| 242 |
+
# --- Hallucination Removal ---
|
| 243 |
+
|
| 244 |
+
@staticmethod
|
| 245 |
+
def remove_hallucinations(text: str) -> str:
|
| 246 |
+
"""Remove model hallucinations: duplicate words, trailing 'و' artifacts."""
|
| 247 |
+
words = text.split()
|
| 248 |
+
if not words:
|
| 249 |
+
return text
|
| 250 |
+
|
| 251 |
+
result = []
|
| 252 |
+
i = 0
|
| 253 |
+
|
| 254 |
+
def normalize_word(w: str) -> str:
|
| 255 |
+
"""Normalize for comparison"""
|
| 256 |
+
w = w.replace('ال', '').replace('ة', 'ه')
|
| 257 |
+
w = re.sub(r'[أإآ]', 'ا', w)
|
| 258 |
+
return w
|
| 259 |
+
|
| 260 |
+
while i < len(words):
|
| 261 |
+
word = words[i]
|
| 262 |
+
|
| 263 |
+
# Remove trailing 'و' artifacts (الماضيةو → الماضية)
|
| 264 |
+
if len(word) > 4 and word.endswith('و'):
|
| 265 |
+
prev_char = word[-2]
|
| 266 |
+
if prev_char in 'ةهاأإآء':
|
| 267 |
+
word = word[:-1]
|
| 268 |
+
|
| 269 |
+
# Check for duplicate patterns
|
| 270 |
+
if i + 1 < len(words):
|
| 271 |
+
next_word = words[i + 1]
|
| 272 |
+
if normalize_word(word) == normalize_word(next_word):
|
| 273 |
+
# Keep the one with 'ال' if possible
|
| 274 |
+
keep = next_word if next_word.startswith('ال') and not word.startswith('ال') else word
|
| 275 |
+
result.append(keep)
|
| 276 |
+
i += 2
|
| 277 |
+
continue
|
| 278 |
+
|
| 279 |
+
result.append(word)
|
| 280 |
+
i += 1
|
| 281 |
+
|
| 282 |
+
return ' '.join(result)
|
| 283 |
+
|
| 284 |
+
@staticmethod
|
| 285 |
+
def remove_hallucinated_prefix(text: str, original: str) -> str:
|
| 286 |
+
"""Remove particles (و/في) added by model if not in original"""
|
| 287 |
+
if not original:
|
| 288 |
+
return text
|
| 289 |
+
|
| 290 |
+
if text.startswith('و ') and not original.startswith('و'):
|
| 291 |
+
rest = text[2:].strip()
|
| 292 |
+
# Verify it matches original
|
| 293 |
+
if AraSpellPostProcessor.normalize_special_chars(rest) == AraSpellPostProcessor.normalize_special_chars(original):
|
| 294 |
+
return rest
|
| 295 |
+
|
| 296 |
+
return text
|
| 297 |
+
|
| 298 |
+
# --- Word Splitting & Merging ---
|
| 299 |
+
|
| 300 |
+
@staticmethod
|
| 301 |
+
def merge_separated_al(text: str) -> str:
|
| 302 |
+
"""Merge 'ال' separated by space: ال + كتاب → الكتاب"""
|
| 303 |
+
return re.sub(r'\bال\s+(\w+)', r'ال\1', text)
|
| 304 |
+
|
| 305 |
+
@staticmethod
|
| 306 |
+
def join_fragments(text: str) -> str:
|
| 307 |
+
"""Join short fragments with validation. e.g. الط + الب → الطالب"""
|
| 308 |
+
words = text.split()
|
| 309 |
+
if len(words) < 2:
|
| 310 |
+
return text
|
| 311 |
+
|
| 312 |
+
# Common standalone words that should NOT be merged
|
| 313 |
+
STANDALONE_WORDS = {
|
| 314 |
+
'من', 'في', 'على', 'عن', 'مع', 'إلى', 'الى', 'حتى', 'منذ', 'خلال',
|
| 315 |
+
'بعد', 'قبل', 'ب', 'ل', 'ك', 'و', 'أو', 'لا', 'ما', 'لم', 'لن',
|
| 316 |
+
'هو', 'هي', 'هم', 'أن', 'إن', 'كل', 'كان', 'قد', 'قال', 'ذلك',
|
| 317 |
+
'هذا', 'هذه', 'تلك', 'التي', 'الذي', 'التى', 'اللذي'
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
result = []
|
| 321 |
+
i = 0
|
| 322 |
+
|
| 323 |
+
while i < len(words):
|
| 324 |
+
word = words[i]
|
| 325 |
+
|
| 326 |
+
if i + 1 < len(words):
|
| 327 |
+
next_word = words[i + 1]
|
| 328 |
+
|
| 329 |
+
# SAFETY: Don't merge if both are standalone words
|
| 330 |
+
if word in STANDALONE_WORDS and next_word in STANDALONE_WORDS:
|
| 331 |
+
result.append(word)
|
| 332 |
+
i += 1
|
| 333 |
+
continue
|
| 334 |
+
|
| 335 |
+
# Case 1: Single char fragment (safe to merge)
|
| 336 |
+
if len(next_word) == 1:
|
| 337 |
+
result.append(word + next_word)
|
| 338 |
+
i += 2
|
| 339 |
+
continue
|
| 340 |
+
|
| 341 |
+
# Case 2: Overlap (last char of word == first char of next)
|
| 342 |
+
if len(word) >= 2 and len(next_word) >= 2 and word[-1] == next_word[0]:
|
| 343 |
+
if not (word in STANDALONE_WORDS and next_word in STANDALONE_WORDS):
|
| 344 |
+
result.append(word[:-1] + next_word)
|
| 345 |
+
i += 2
|
| 346 |
+
continue
|
| 347 |
+
|
| 348 |
+
# Case 3: Short fragments (2-4 chars + 1-2 chars)
|
| 349 |
+
if (2 <= len(word) <= 4 and
|
| 350 |
+
1 <= len(next_word) <= 2 and
|
| 351 |
+
3 <= len(word) + len(next_word) <= 7):
|
| 352 |
+
if not (word in STANDALONE_WORDS and next_word in STANDALONE_WORDS):
|
| 353 |
+
result.append(word + next_word)
|
| 354 |
+
i += 2
|
| 355 |
+
continue
|
| 356 |
+
|
| 357 |
+
result.append(word)
|
| 358 |
+
i += 1
|
| 359 |
+
|
| 360 |
+
return ' '.join(result)
|
| 361 |
+
|
| 362 |
+
# --- Main Pipelines ---
|
| 363 |
+
|
| 364 |
+
@staticmethod
|
| 365 |
+
def full_postprocess(text: str, original: str = "", vocab_manager=None) -> str:
|
| 366 |
+
"""
|
| 367 |
+
Apply all post-processing steps (OPTIMIZED ORDER!)
|
| 368 |
+
vocab_manager: optional, enables smart ه/ة handling
|
| 369 |
+
"""
|
| 370 |
+
# 1. Remove hallucinated prefixes
|
| 371 |
+
if original:
|
| 372 |
+
text = AraSpellPostProcessor.remove_hallucinated_prefix(text, original)
|
| 373 |
+
|
| 374 |
+
# 2. Basic normalization
|
| 375 |
+
text = AraSpellPostProcessor.normalize_special_chars(text)
|
| 376 |
+
|
| 377 |
+
# 3. Remove hallucinations
|
| 378 |
+
text = AraSpellPostProcessor.remove_hallucinations(text)
|
| 379 |
+
|
| 380 |
+
# 4. Collapse repetitions (UNIFIED!)
|
| 381 |
+
text = AraSpellPostProcessor.unified_collapse_repeated(text)
|
| 382 |
+
|
| 383 |
+
# 5. Fix Hamza (CONSERVATIVE!)
|
| 384 |
+
text = AraSpellPostProcessor.fix_hamza_conservative(text)
|
| 385 |
+
|
| 386 |
+
# 6. Fix Ta Marbuta (SMART MODE with vocab_manager!)
|
| 387 |
+
text = AraSpellPostProcessor.fix_ha_ta_marbuta(text, vocab_manager=vocab_manager)
|
| 388 |
+
|
| 389 |
+
# 7. Remove word repetition with 'و'
|
| 390 |
+
text = AraSpellPostProcessor.remove_word_repetition_with_wa(text)
|
| 391 |
+
|
| 392 |
+
# 8. Remove duplicate words
|
| 393 |
+
text = AraSpellPostProcessor.remove_duplicate_words(text)
|
| 394 |
+
|
| 395 |
+
# 9. Final space normalization
|
| 396 |
+
text = AraSpellPostProcessor.normalize_spaces(text)
|
| 397 |
+
|
| 398 |
+
return text
|
| 399 |
+
|
| 400 |
+
|
| 401 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 402 |
+
# ERROR CLASSIFIER
|
| 403 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 404 |
+
|
| 405 |
+
class ErrorClassifier:
|
| 406 |
+
"""Classify type of spelling error"""
|
| 407 |
+
|
| 408 |
+
NON_ARABIC_KEYBOARD = set('پگچژکەڕڤڵڎےۀۃھیټډڼڑ')
|
| 409 |
+
|
| 410 |
+
@staticmethod
|
| 411 |
+
def has_char_substitution(text: str) -> bool:
|
| 412 |
+
return any(c in ErrorClassifier.NON_ARABIC_KEYBOARD for c in text)
|
| 413 |
+
|
| 414 |
+
@staticmethod
|
| 415 |
+
def has_char_repetition(text: str, threshold: int = 3) -> bool:
|
| 416 |
+
return bool(re.search(r"(.)\1{" + str(threshold - 1) + ",}", text))
|
| 417 |
+
|
| 418 |
+
@staticmethod
|
| 419 |
+
def has_word_merge(text: str, max_word_len: int = 8) -> bool:
|
| 420 |
+
words = text.split()
|
| 421 |
+
if any(len(w) > max_word_len for w in words):
|
| 422 |
+
return True
|
| 423 |
+
if len(words) == 1 and len(text) > 6:
|
| 424 |
+
return True
|
| 425 |
+
return False
|
| 426 |
+
|
| 427 |
+
@staticmethod
|
| 428 |
+
def classify(text: str) -> ErrorType:
|
| 429 |
+
"""Classify the error type"""
|
| 430 |
+
has_rep = ErrorClassifier.has_char_repetition(text)
|
| 431 |
+
has_merge = ErrorClassifier.has_word_merge(text)
|
| 432 |
+
has_sub = ErrorClassifier.has_char_substitution(text)
|
| 433 |
+
|
| 434 |
+
error_count = sum([has_rep, has_merge, has_sub])
|
| 435 |
+
|
| 436 |
+
if error_count >= 2:
|
| 437 |
+
return ErrorType.MIXED
|
| 438 |
+
elif has_sub:
|
| 439 |
+
return ErrorType.CHAR_SUBSTITUTION
|
| 440 |
+
elif has_rep:
|
| 441 |
+
return ErrorType.CHAR_REPETITION
|
| 442 |
+
elif has_merge:
|
| 443 |
+
return ErrorType.WORD_MERGE
|
| 444 |
+
else:
|
| 445 |
+
return ErrorType.CLEAN
|
| 446 |
+
|
| 447 |
+
# ═══════════════════════════════════════════════════════════════════════════════
|
| 448 |
+
# RULES-BASED CORRECTOR
|
| 449 |
+
# ═══════════════════════════════════════════════════════════════════════════════
|
| 450 |
+
|
| 451 |
+
class RulesBasedCorrector:
|
| 452 |
+
"""Rules-based correction with keyboard proximity mapping."""
|
| 453 |
+
|
| 454 |
+
# Persian/Urdu → Arabic mapping
|
| 455 |
+
SUBSTITUTION_MAP = {
|
| 456 |
+
'ک': 'ك', 'ی': 'ي', 'ے': 'ي',
|
| 457 |
+
'پ': 'ب', 'چ': 'ج', 'ژ': 'ز',
|
| 458 |
+
'گ': 'ك', 'ڤ': 'ف', 'ڵ': 'ل',
|
| 459 |
+
'ڕ': 'ر', 'ڎ': 'د', 'ڼ': 'ن',
|
| 460 |
+
'ټ': 'ت', 'ډ': 'د', 'ړ': 'ر',
|
| 461 |
+
'ۀ': 'ه', 'ۃ': 'ة', 'ھ': 'ه',
|
| 462 |
+
'ە': 'ه', 'ڑ': 'ر'
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
# EXPANDED: 16 prepositions instead of 2
|
| 466 |
+
PREPOSITIONS = {
|
| 467 |
+
'من', 'في', 'على', 'عن', 'مع', 'إلى', 'الى',
|
| 468 |
+
'حتى', 'منذ', 'خلال', 'بعد', 'قبل',
|
| 469 |
+
'ب', 'ل', 'ك',
|
| 470 |
+
'لل'
|
| 471 |
+
}
|
| 472 |
+
|
| 473 |
+
# Keyboard Proximity Mapping
|
| 474 |
+
# Arabic keyboard layout adjacency
|
| 475 |
+
KEYBOARD_NEIGHBORS = {
|
| 476 |
+
'ض': ['ص', 'ق'],
|
| 477 |
+
'ص': ['ض', 'ث', 'ق'],
|
| 478 |
+
'ث': ['ص', 'ق'],
|
| 479 |
+
'ق': ['ض', 'ص', 'ث', 'ف', 'غ'],
|
| 480 |
+
'ف': ['ق', 'غ', 'ع', 'ب'],
|
| 481 |
+
'غ': ['ق', 'ف', 'ع', 'ه'],
|
| 482 |
+
'ع': ['ف', 'غ', 'ه', 'خ'],
|
| 483 |
+
'ه': ['غ', 'ع', 'خ', 'ح'],
|
| 484 |
+
'خ': ['ع', 'ه', 'ح', 'ج'],
|
| 485 |
+
'ح': ['ه', 'خ', 'ج'],
|
| 486 |
+
'ج': ['خ', 'ح', 'د'],
|
| 487 |
+
'د': ['ج', 'ذ'],
|
| 488 |
+
'ذ': ['د'],
|
| 489 |
+
'ش': ['س', 'ي', 'ئ'],
|
| 490 |
+
'س': ['ش', 'ي', 'ب'],
|
| 491 |
+
'ي': ['ش', 'س', 'ب', 'ت'],
|
| 492 |
+
'ب': ['ي', 'س', 'ف', 'ل', 'ن'],
|
| 493 |
+
'ل': ['ب', 'ا', 'ن', 'م'],
|
| 494 |
+
'ا': ['ل', 'ت', 'م'],
|
| 495 |
+
'ت': ['ي', 'ا', 'ن'],
|
| 496 |
+
'ن': ['ب', 'ل', 'ت', 'م', 'ك'],
|
| 497 |
+
'م': ['ل', 'ا', 'ن', 'ك'],
|
| 498 |
+
'ك': ['ن', 'م', 'ط'],
|
| 499 |
+
'ط': ['ك', 'ظ'],
|
| 500 |
+
'ظ': ['ط'],
|
| 501 |
+
'ئ': ['ش', 'ء', 'ر'],
|
| 502 |
+
'ء': ['ئ', 'ؤ'],
|
| 503 |
+
'ؤ': ['ء', 'ر'],
|
| 504 |
+
'ر': ['ئ', 'ؤ', 'لا', 'ى', 'ز'],
|
| 505 |
+
'لا': ['ر', 'ى'],
|
| 506 |
+
'ى': ['ر', 'لا', 'ة', 'ز'],
|
| 507 |
+
'ة': ['ى', 'و', 'ز'],
|
| 508 |
+
'و': ['ة', 'ز'],
|
| 509 |
+
'ز': ['ر', 'ى', 'ة', 'و'],
|
| 510 |
+
# Alif variants
|
| 511 |
+
'أ': ['ا', 'إ', 'آ'],
|
| 512 |
+
'إ': ['ا', 'أ'],
|
| 513 |
+
'آ': ['ا', 'أ'],
|
| 514 |
+
}
|
| 515 |
+
|
| 516 |
+
@staticmethod
|
| 517 |
+
def is_keyboard_neighbor(char1: str, char2: str) -> bool:
|
| 518 |
+
"""Check if two Arabic chars are adjacent on keyboard."""
|
| 519 |
+
neighbors = RulesBasedCorrector.KEYBOARD_NEIGHBORS.get(char1, [])
|
| 520 |
+
return char2 in neighbors
|
| 521 |
+
|
| 522 |
+
@staticmethod
|
| 523 |
+
def fix_char_substitution(text: str) -> str:
|
| 524 |
+
"""Replace Persian/Urdu characters with Arabic"""
|
| 525 |
+
for old, new in RulesBasedCorrector.SUBSTITUTION_MAP.items():
|
| 526 |
+
text = text.replace(old, new)
|
| 527 |
+
return text
|
| 528 |
+
|
| 529 |
+
@staticmethod
|
| 530 |
+
def fix_char_repetition(text: str) -> str:
|
| 531 |
+
"""Remove excessive character repetition (3+ consecutive → 1)."""
|
| 532 |
+
# Only collapse 3+ repetitions (not 2+)
|
| 533 |
+
text = re.sub(r'([^\d\s])\1{2,}', r'\1', text)
|
| 534 |
+
return text
|
| 535 |
+
|
| 536 |
+
@staticmethod
|
| 537 |
+
def advanced_heuristic_repair(text: str) -> str:
|
| 538 |
+
"""
|
| 539 |
+
Apply aggressive heuristic repairs to generate a strong baseline candidate.
|
| 540 |
+
1. Unified Char Fixes (Persian/Urdu + Repetition)
|
| 541 |
+
2. Aggressive Word Splitting (Iterative & Anchored)
|
| 542 |
+
"""
|
| 543 |
+
# 1. Base Fixes
|
| 544 |
+
text = RulesBasedCorrector.fix_char_substitution(text)
|
| 545 |
+
text = RulesBasedCorrector.fix_char_repetition(text)
|
| 546 |
+
|
| 547 |
+
# 2. Heuristic Split
|
| 548 |
+
words = text.split()
|
| 549 |
+
processed_words = []
|
| 550 |
+
for word in words:
|
| 551 |
+
processed_words.append(RulesBasedCorrector._recursive_split(word))
|
| 552 |
+
|
| 553 |
+
return ' '.join(processed_words)
|
| 554 |
+
|
| 555 |
+
@staticmethod
|
| 556 |
+
def _recursive_split(word: str) -> str:
|
| 557 |
+
"""
|
| 558 |
+
Recursively split merged words (Anchored to Start)
|
| 559 |
+
Avoids splitting 'المنزل' -> 'ال من زل' (middle split)
|
| 560 |
+
"""
|
| 561 |
+
if len(word) < 4:
|
| 562 |
+
return word
|
| 563 |
+
|
| 564 |
+
# 1. Separable Prepositions (Must be at START)
|
| 565 |
+
# "فيالبيت" -> "في البيت"
|
| 566 |
+
separables = sorted(['من', 'في', 'على', 'عن', 'مع', 'إلى', 'الى', 'حتى', 'منذ', 'خلال', 'بعد', 'قبل'], key=len, reverse=True)
|
| 567 |
+
|
| 568 |
+
for sep in separables:
|
| 569 |
+
# Check matches: exact match or prefix match
|
| 570 |
+
if word == sep:
|
| 571 |
+
return word
|
| 572 |
+
|
| 573 |
+
if word.startswith(sep):
|
| 574 |
+
remainder = word[len(sep):]
|
| 575 |
+
# Condition: Remainder must be substantial (usually starts with al- or len > 2)
|
| 576 |
+
if len(remainder) >= 3:
|
| 577 |
+
# Recursive call on remainder
|
| 578 |
+
return sep + " " + RulesBasedCorrector._recursive_split(remainder)
|
| 579 |
+
|
| 580 |
+
# 2. Common typo merges (e.g. "يا" + Name)
|
| 581 |
+
if word.startswith('يا') and len(word) > 4:
|
| 582 |
+
return 'يا ' + RulesBasedCorrector._recursive_split(word[2:])
|
| 583 |
+
|
| 584 |
+
# 3. Attached Particles (Only 'Wa' and 'Fa' are commonly mistakenly merged with non-al words in typos)
|
| 585 |
+
# "وال" -> "و ال" is usually correct in tokenization but "و" is attached in script.
|
| 586 |
+
# We only split if it looks like a HARD merge error.
|
| 587 |
+
|
| 588 |
+
return word
|
| 589 |
+
|
| 590 |
+
|
| 591 |
+
|
| 592 |
+
|
| 593 |
+
# ═══════════════════════════════════════════════════════════════════════════════
|
| 594 |
+
# OUTPUT VALIDATOR (Hallucination Prevention)
|
| 595 |
+
# ═══════════════════════════════════════════════════════════════════════════════
|
| 596 |
+
|
| 597 |
+
class OutputValidator:
|
| 598 |
+
"""Validate model outputs to prevent hallucinations"""
|
| 599 |
+
|
| 600 |
+
@staticmethod
|
| 601 |
+
def calculate_edit_distance(s1: str, s2: str) -> int:
|
| 602 |
+
"""Calculate Levenshtein distance"""
|
| 603 |
+
return Levenshtein.distance(s1, s2)
|
| 604 |
+
|
| 605 |
+
@staticmethod
|
| 606 |
+
def check_character_preservation(original: str, corrected: str) -> Tuple[bool, str]:
|
| 607 |
+
"""Check if characters are mostly preserved (Jaccard similarity)"""
|
| 608 |
+
chars_original = set(original)
|
| 609 |
+
chars_corrected = set(corrected)
|
| 610 |
+
|
| 611 |
+
if not chars_original:
|
| 612 |
+
return True, "valid"
|
| 613 |
+
|
| 614 |
+
intersection = chars_original & chars_corrected
|
| 615 |
+
union = chars_original | chars_corrected
|
| 616 |
+
|
| 617 |
+
jaccard = len(intersection) / len(union) if union else 0
|
| 618 |
+
|
| 619 |
+
if jaccard < 0.35:
|
| 620 |
+
return False, "low_character_similarity"
|
| 621 |
+
|
| 622 |
+
return True, "valid"
|
| 623 |
+
|
| 624 |
+
@staticmethod
|
| 625 |
+
def check_word_count(original: str, corrected: str) -> Tuple[bool, str]:
|
| 626 |
+
"""
|
| 627 |
+
Check if word count is reasonable
|
| 628 |
+
Relaxed: Allow splitting merged words (count can double)
|
| 629 |
+
"""
|
| 630 |
+
len_orig = len(original.split())
|
| 631 |
+
len_corr = len(corrected.split())
|
| 632 |
+
|
| 633 |
+
# Allow expanding 1 word to up to 3 (e.g. "فيالمدرسة" -> "في المدرسة")
|
| 634 |
+
if len_orig == 1:
|
| 635 |
+
if len_corr <= 3:
|
| 636 |
+
return True, "valid"
|
| 637 |
+
# If original is very long, allow more splits (e.g. "هذاالولدذهبإلىالمدرسة")
|
| 638 |
+
if len(original) > 12 and len_corr <= 6:
|
| 639 |
+
return True, "valid"
|
| 640 |
+
|
| 641 |
+
# For sentences, stricter ratio
|
| 642 |
+
ratio = len_corr / len_orig if len_orig > 0 else 0
|
| 643 |
+
if ratio > 2.0 or ratio < 0.5:
|
| 644 |
+
return False, "word_count_mismatch"
|
| 645 |
+
|
| 646 |
+
return True, "valid"
|
| 647 |
+
|
| 648 |
+
def validate(self, original: str, corrected: str, error_type: str) -> Tuple[bool, str]:
|
| 649 |
+
"""
|
| 650 |
+
Main validation logic
|
| 651 |
+
"""
|
| 652 |
+
# 0. Sanity Check
|
| 653 |
+
if not corrected or not corrected.strip():
|
| 654 |
+
return False, "empty_output"
|
| 655 |
+
|
| 656 |
+
# Space Leniency: if ONLY difference is whitespace → accept
|
| 657 |
+
original_no_space = original.replace(' ', '').replace('\u200c', '') # Also handle ZWNJ
|
| 658 |
+
corrected_no_space = corrected.replace(' ', '').replace('\u200c', '')
|
| 659 |
+
|
| 660 |
+
if original_no_space == corrected_no_space:
|
| 661 |
+
# Only whitespace changed - accept immediately
|
| 662 |
+
return True, "space_leniency_accept"
|
| 663 |
+
|
| 664 |
+
# 1. Length Ratio Check
|
| 665 |
+
len_orig = len(original)
|
| 666 |
+
len_corr = len(corrected)
|
| 667 |
+
|
| 668 |
+
# Allow expansion for word splitting
|
| 669 |
+
if len_corr > len_orig * 2.5:
|
| 670 |
+
return False, "too_long"
|
| 671 |
+
|
| 672 |
+
# Allow shrinking (but not typically more than 50% unless removing repetition)
|
| 673 |
+
if len_corr < len_orig * 0.5:
|
| 674 |
+
# Exception: if original had excessive repetition
|
| 675 |
+
if error_type == ErrorType.CHAR_REPETITION:
|
| 676 |
+
pass
|
| 677 |
+
else:
|
| 678 |
+
return False, "too_short"
|
| 679 |
+
|
| 680 |
+
# 2. Check Word Count
|
| 681 |
+
is_valid_count, reason = self.check_word_count(original, corrected)
|
| 682 |
+
if not is_valid_count:
|
| 683 |
+
return False, reason
|
| 684 |
+
|
| 685 |
+
# 3. Check Character Preservation
|
| 686 |
+
# Critical for avoiding hallucinations
|
| 687 |
+
is_valid_chars, reason = self.check_character_preservation(original, corrected)
|
| 688 |
+
if not is_valid_chars:
|
| 689 |
+
# Exception: If input was garbage/keyboard mash, preservation might be low.
|
| 690 |
+
# But for valid inputs, this prevents changing "كتاب" to "مكتبة" (if no roots match)
|
| 691 |
+
return False, reason
|
| 692 |
+
|
| 693 |
+
return True, "valid"
|
| 694 |
+
|
| 695 |
+
# ═══════════════════════════════════════════════════════════════════════════════
|
| 696 |
+
# VOCABULARY MANAGER
|
| 697 |
+
# ═══════════════════════════════════════════════════════════════════════════════
|
| 698 |
+
|
| 699 |
+
class VocabularyManager:
|
| 700 |
+
"""
|
| 701 |
+
Centralized vocabulary management for OOV/IV detection.
|
| 702 |
+
Key for vocabulary-aware acceptance: OOV→IV = accept, IV→OOV = reject.
|
| 703 |
+
"""
|
| 704 |
+
|
| 705 |
+
# Arabic character equivalence for normalization
|
| 706 |
+
HAMZA_VARIANTS = {'أ', 'إ', 'آ', 'ء', 'ؤ', 'ئ', 'ا'}
|
| 707 |
+
ALEF_NORMALIZED = 'ا'
|
| 708 |
+
TA_MARBUTA = 'ة'
|
| 709 |
+
HA = 'ه'
|
| 710 |
+
YA_VARIANTS = {'ي', 'ى'}
|
| 711 |
+
YA_NORMALIZED = 'ي'
|
| 712 |
+
|
| 713 |
+
def __init__(self, tokenizer):
|
| 714 |
+
self.tokenizer = tokenizer
|
| 715 |
+
|
| 716 |
+
# Build vocabulary set from tokenizer (exclude subwords and short tokens)
|
| 717 |
+
self.vocab = {
|
| 718 |
+
w for w in tokenizer.get_vocab().keys()
|
| 719 |
+
if w.isalpha() and not w.startswith('##') and len(w) > 1
|
| 720 |
+
}
|
| 721 |
+
|
| 722 |
+
# Frequency rank: lower index = more common (usually)
|
| 723 |
+
self.vocab_rank = {w: i for w, i in tokenizer.get_vocab().items()}
|
| 724 |
+
|
| 725 |
+
# Build normalized vocabulary for fuzzy matching
|
| 726 |
+
self.normalized_vocab = {self.normalize_for_comparison(w): w for w in self.vocab}
|
| 727 |
+
|
| 728 |
+
logger.info(f"VocabularyManager initialized: {len(self.vocab)} words")
|
| 729 |
+
|
| 730 |
+
@classmethod
|
| 731 |
+
def normalize_for_comparison(cls, word: str) -> str:
|
| 732 |
+
"""
|
| 733 |
+
Normalize Arabic word for comparison (hamza, ta marbuta, etc.)
|
| 734 |
+
Used for equivalence checking, not for final output.
|
| 735 |
+
"""
|
| 736 |
+
result = []
|
| 737 |
+
for i, char in enumerate(word):
|
| 738 |
+
# Normalize Hamza variants to Alef
|
| 739 |
+
if char in cls.HAMZA_VARIANTS:
|
| 740 |
+
result.append(cls.ALEF_NORMALIZED)
|
| 741 |
+
# Normalize Ta Marbuta to Ha at word end
|
| 742 |
+
elif char == cls.TA_MARBUTA and i == len(word) - 1:
|
| 743 |
+
result.append(cls.HA)
|
| 744 |
+
# Normalize Ya variants
|
| 745 |
+
elif char in cls.YA_VARIANTS:
|
| 746 |
+
result.append(cls.YA_NORMALIZED)
|
| 747 |
+
else:
|
| 748 |
+
result.append(char)
|
| 749 |
+
return ''.join(result)
|
| 750 |
+
|
| 751 |
+
def is_iv(self, word: str) -> bool:
|
| 752 |
+
"""Check if word is In-Vocabulary (known word)."""
|
| 753 |
+
clean = re.sub(r'[^\w]', '', word)
|
| 754 |
+
if not clean:
|
| 755 |
+
return True # Empty/punctuation only = treat as valid
|
| 756 |
+
|
| 757 |
+
# Direct check
|
| 758 |
+
if clean in self.vocab:
|
| 759 |
+
return True
|
| 760 |
+
|
| 761 |
+
# Normalized check (handles hamza/ta marbuta variations)
|
| 762 |
+
normalized = self.normalize_for_comparison(clean)
|
| 763 |
+
if normalized in self.normalized_vocab:
|
| 764 |
+
return True
|
| 765 |
+
|
| 766 |
+
return False
|
| 767 |
+
|
| 768 |
+
def is_oov(self, word: str) -> bool:
|
| 769 |
+
"""Check if word is Out-Of-Vocabulary (unknown word)."""
|
| 770 |
+
return not self.is_iv(word)
|
| 771 |
+
|
| 772 |
+
def get_frequency_rank(self, word: str) -> int:
|
| 773 |
+
"""Get frequency rank (lower = more common). Returns 999999 for OOV."""
|
| 774 |
+
clean = re.sub(r'[^\w]', '', word)
|
| 775 |
+
return self.vocab_rank.get(clean, 999999)
|
| 776 |
+
|
| 777 |
+
def all_words_iv(self, text: str) -> bool:
|
| 778 |
+
"""Check if ALL words in text are In-Vocabulary."""
|
| 779 |
+
words = text.split()
|
| 780 |
+
return all(self.is_iv(w) for w in words)
|
| 781 |
+
|
| 782 |
+
def count_oov_words(self, text: str) -> int:
|
| 783 |
+
"""Count number of OOV words in text."""
|
| 784 |
+
words = text.split()
|
| 785 |
+
return sum(1 for w in words if self.is_oov(w))
|
| 786 |
+
|
| 787 |
+
def get_oov_words(self, text: str) -> List[str]:
|
| 788 |
+
"""Get list of OOV words in text."""
|
| 789 |
+
words = text.split()
|
| 790 |
+
return [w for w in words if self.is_oov(w)]
|
| 791 |
+
|
| 792 |
+
def words_are_equivalent(self, word1: str, word2: str) -> bool:
|
| 793 |
+
"""
|
| 794 |
+
Check if two words are equivalent (considering Arabic character variations).
|
| 795 |
+
Useful for accepting corrections that only differ in hamza/ta marbuta.
|
| 796 |
+
"""
|
| 797 |
+
norm1 = self.normalize_for_comparison(word1)
|
| 798 |
+
norm2 = self.normalize_for_comparison(word2)
|
| 799 |
+
return norm1 == norm2
|
| 800 |
+
|
| 801 |
+
@staticmethod
|
| 802 |
+
def damerau_levenshtein_distance(s1: str, s2: str) -> int:
|
| 803 |
+
"""
|
| 804 |
+
Calculate Damerau-Levenshtein distance (transpositions count as 1 edit).
|
| 805 |
+
This is better for Arabic typos like اقصتاديا→اقتصاديا (swap صت→تص).
|
| 806 |
+
"""
|
| 807 |
+
return jellyfish.damerau_levenshtein_distance(s1, s2)
|
| 808 |
+
|
| 809 |
+
def calculate_similarity(self, original: str, corrected: str) -> float:
|
| 810 |
+
"""
|
| 811 |
+
Calculate similarity score using Damerau-Levenshtein distance.
|
| 812 |
+
Returns value between 0 and 1 (1 = identical).
|
| 813 |
+
"""
|
| 814 |
+
dist = self.damerau_levenshtein_distance(original, corrected)
|
| 815 |
+
max_len = max(len(original), len(corrected), 1)
|
| 816 |
+
return 1.0 - (dist / max_len)
|
| 817 |
+
|
| 818 |
+
# ═══════════════════════════════════════════════════════════════════════════════
|
| 819 |
+
# WORD ALIGNER
|
| 820 |
+
# ═══════════════════════════════════════════════════════════════════════════════
|
| 821 |
+
|
| 822 |
+
class WordAligner:
|
| 823 |
+
"""
|
| 824 |
+
Aligns input and output words to create hybrid corrections.
|
| 825 |
+
Helps when model fixes one word but breaks another (Raw Wins/Both Wrong cause).
|
| 826 |
+
"""
|
| 827 |
+
|
| 828 |
+
def __init__(self, vocab_manager):
|
| 829 |
+
"""Initialize with VocabularyManager for IV checks."""
|
| 830 |
+
self.vocab = vocab_manager
|
| 831 |
+
|
| 832 |
+
def align_words(self, input_text: str, output_text: str) -> str:
|
| 833 |
+
"""
|
| 834 |
+
Create hybrid by selecting best word from each position.
|
| 835 |
+
Uses simple space-based alignment (works for most Arabic cases).
|
| 836 |
+
"""
|
| 837 |
+
input_words = input_text.split()
|
| 838 |
+
output_words = output_text.split()
|
| 839 |
+
|
| 840 |
+
# If lengths differ significantly, alignment is risky -> fallback to output
|
| 841 |
+
if abs(len(input_words) - len(output_words)) > 2:
|
| 842 |
+
input_oov = self.vocab.count_oov_words(input_text)
|
| 843 |
+
output_oov = self.vocab.count_oov_words(output_text)
|
| 844 |
+
return output_text if output_oov < input_oov else input_text
|
| 845 |
+
|
| 846 |
+
result = []
|
| 847 |
+
|
| 848 |
+
# Simple position-based alignment (min length)
|
| 849 |
+
min_len = min(len(input_words), len(output_words))
|
| 850 |
+
|
| 851 |
+
for i in range(min_len):
|
| 852 |
+
in_word = input_words[i]
|
| 853 |
+
out_word = output_words[i]
|
| 854 |
+
|
| 855 |
+
best_word = self._select_best_word(in_word, out_word)
|
| 856 |
+
result.append(best_word)
|
| 857 |
+
|
| 858 |
+
# Append remaining words from the longer sequence
|
| 859 |
+
if len(output_words) > min_len:
|
| 860 |
+
result.extend(output_words[min_len:])
|
| 861 |
+
elif len(input_words) > min_len:
|
| 862 |
+
# If input is longer, verify if trailing words are IV
|
| 863 |
+
# If trailing input words are OOV, maybe model was right to remove them?
|
| 864 |
+
# Safest is to keep them if they are IV, else drop.
|
| 865 |
+
for w in input_words[min_len:]:
|
| 866 |
+
if self.vocab.is_iv(w):
|
| 867 |
+
result.append(w)
|
| 868 |
+
|
| 869 |
+
return ' '.join(result)
|
| 870 |
+
|
| 871 |
+
def _select_best_word(self, input_word: str, output_word: str) -> str:
|
| 872 |
+
"""
|
| 873 |
+
Select best word between input and output version.
|
| 874 |
+
|
| 875 |
+
Logic:
|
| 876 |
+
1. Input OOV + Output IV → Take Output (Model fixed it)
|
| 877 |
+
2. Input IV + Output OOV → Keep Input (Model broke it)
|
| 878 |
+
3. Input IV + Output IV → Keep Input (Conservative) unless Output is much better?
|
| 879 |
+
- For now, strict conservative: if input is valid, keep it.
|
| 880 |
+
4. Both OOV → Take Output (Model likely closer)
|
| 881 |
+
"""
|
| 882 |
+
if input_word == output_word:
|
| 883 |
+
return input_word
|
| 884 |
+
|
| 885 |
+
in_iv = self.vocab.is_iv(input_word)
|
| 886 |
+
out_iv = self.vocab.is_iv(output_word)
|
| 887 |
+
|
| 888 |
+
# Case 1: Correction worked (OOV -> IV)
|
| 889 |
+
if not in_iv and out_iv:
|
| 890 |
+
return output_word
|
| 891 |
+
|
| 892 |
+
# Case 2: Correction broke it (IV -> OOV)
|
| 893 |
+
if in_iv and not out_iv:
|
| 894 |
+
return input_word
|
| 895 |
+
|
| 896 |
+
# Case 3: Both IV (Semantic change or split/merge)
|
| 897 |
+
# Conservative: Keep input to avoid semantic drift (Contextual errors are rare compared to typos)
|
| 898 |
+
if in_iv and out_iv:
|
| 899 |
+
return input_word
|
| 900 |
+
|
| 901 |
+
# Case 4: Both OOV
|
| 902 |
+
# Subword-level correction
|
| 903 |
+
# If words are similar length, try character-level blending to find IV
|
| 904 |
+
if len(input_word) == len(output_word) and len(input_word) >= 3:
|
| 905 |
+
# Try replacing one char at a time from output into input
|
| 906 |
+
for i in range(len(input_word)):
|
| 907 |
+
if input_word[i] != output_word[i]:
|
| 908 |
+
# Try input with this one char from output
|
| 909 |
+
hybrid = input_word[:i] + output_word[i] + input_word[i+1:]
|
| 910 |
+
if self.vocab.is_iv(hybrid):
|
| 911 |
+
return hybrid
|
| 912 |
+
# Try output with this one char from input
|
| 913 |
+
hybrid2 = output_word[:i] + input_word[i] + output_word[i+1:]
|
| 914 |
+
if self.vocab.is_iv(hybrid2):
|
| 915 |
+
return hybrid2
|
| 916 |
+
|
| 917 |
+
# Default: Take output, usually closer to target even if still OOV
|
| 918 |
+
return output_word
|
| 919 |
+
|
| 920 |
+
# ═══════════════════════════════════════════════════════════════════════════════
|
| 921 |
+
# SPLIT/MERGE SPECIALIST
|
| 922 |
+
# ═══════════════════════════════════════════════════════════════════════════════
|
| 923 |
+
|
| 924 |
+
class SplitMergeSpecialist:
|
| 925 |
+
"""
|
| 926 |
+
Handles word splitting and merging with vocabulary validation.
|
| 927 |
+
|
| 928 |
+
Key patterns:
|
| 929 |
+
1. SPLIT: OOV word that can be split into two IV words
|
| 930 |
+
- فيالغالب → في الغالب
|
| 931 |
+
- يقعبجماعة → يقع بجماعة
|
| 932 |
+
2. MERGE: Adjacent OOV fragments that can merge to IV
|
| 933 |
+
- السوري ة → السورية (ta-marbuta attachment)
|
| 934 |
+
- ال كتاب → الكتاب
|
| 935 |
+
"""
|
| 936 |
+
|
| 937 |
+
# Common Arabic prefixes that can be detached
|
| 938 |
+
SEPARABLE_PREFIXES = [
|
| 939 |
+
# Prepositions (longer first for greedy matching)
|
| 940 |
+
'من', 'في', 'على', 'عن', 'مع', 'إلى', 'الى', 'حتى', 'منذ', 'خلال',
|
| 941 |
+
'بعد', 'قبل', 'بين', 'حول', 'تحت', 'فوق', 'أمام', 'وراء', 'دون',
|
| 942 |
+
# Particles
|
| 943 |
+
'أن', 'لن', 'لم', 'قد', 'سوف', 'كي', 'إذا', 'لو', 'مثل', 'غير',
|
| 944 |
+
# Call particle
|
| 945 |
+
'يا',
|
| 946 |
+
]
|
| 947 |
+
|
| 948 |
+
# Protected short words that shouldn't be split
|
| 949 |
+
PROTECTED_WORDS = {
|
| 950 |
+
'في', 'من', 'على', 'عن', 'مع', 'إلى', 'الى', 'ان', 'أن', 'لا', 'ما', 'هو', 'هي',
|
| 951 |
+
'لم', 'لن', 'قد', 'كل', 'كان', 'ذلك', 'هذا', 'هذه', 'التي', 'الذي', 'بين',
|
| 952 |
+
}
|
| 953 |
+
|
| 954 |
+
def __init__(self, vocab_manager):
|
| 955 |
+
"""Initialize with VocabularyManager for IV checks."""
|
| 956 |
+
self.vocab = vocab_manager
|
| 957 |
+
self.separable_prefixes = sorted(
|
| 958 |
+
self.SEPARABLE_PREFIXES, key=len, reverse=True
|
| 959 |
+
)
|
| 960 |
+
|
| 961 |
+
# Attached prefix patterns that should NOT be split (normal Arabic word formations)
|
| 962 |
+
ATTACHED_PREFIXES = [
|
| 963 |
+
'وال', 'بال', 'فال', 'كال', 'لل', # Conjunction/Preposition + Article
|
| 964 |
+
'وب', 'وف', 'ول', 'وك', 'وم', 'ون', # Conjunction + Preposition
|
| 965 |
+
'فب', 'فل', 'فك', 'فم', # Conjunction + Preposition
|
| 966 |
+
]
|
| 967 |
+
|
| 968 |
+
def split_word(self, word: str) -> str:
|
| 969 |
+
"""
|
| 970 |
+
Try to split an OOV word into IV components.
|
| 971 |
+
|
| 972 |
+
Strict Strategy:
|
| 973 |
+
- Only split when BOTH parts are IV
|
| 974 |
+
- Protect attached prefix patterns (وال، بال، etc.)
|
| 975 |
+
- Minimum part lengths to prevent micro-splits
|
| 976 |
+
"""
|
| 977 |
+
# Short words: don't split (increased from 4 to 5 for safety)
|
| 978 |
+
if len(word) < 5:
|
| 979 |
+
return word
|
| 980 |
+
|
| 981 |
+
# Already IV: no need to split
|
| 982 |
+
if self.vocab.is_iv(word):
|
| 983 |
+
return word
|
| 984 |
+
|
| 985 |
+
# Protected words: don't split
|
| 986 |
+
if word in self.PROTECTED_WORDS:
|
| 987 |
+
return word
|
| 988 |
+
|
| 989 |
+
# Protected prefix patterns (وال، بال، فال، etc.)
|
| 990 |
+
# These are normal Arabic word formations, NOT merge errors
|
| 991 |
+
for prefix in self.ATTACHED_PREFIXES:
|
| 992 |
+
if word.startswith(prefix):
|
| 993 |
+
remainder = word[len(prefix):]
|
| 994 |
+
# If the remainder (without the prefix) is IV, this is a valid prefixed word
|
| 995 |
+
if self.vocab.is_iv(remainder):
|
| 996 |
+
return word # Don't split — it's prefix+valid_word
|
| 997 |
+
# Also check with article: e.g. والخصوصي → وال+خصوصي, check خصوصي
|
| 998 |
+
if prefix.endswith('ال') and self.vocab.is_iv(remainder):
|
| 999 |
+
return word
|
| 1000 |
+
|
| 1001 |
+
# 1. Try separable prefixes first (higher priority)
|
| 1002 |
+
for prefix in self.separable_prefixes:
|
| 1003 |
+
if word.startswith(prefix) and len(word) > len(prefix) + 2: # Remainder must be > 2 chars
|
| 1004 |
+
remainder = word[len(prefix):]
|
| 1005 |
+
|
| 1006 |
+
# Only accept if remainder is IV
|
| 1007 |
+
if self.vocab.is_iv(remainder):
|
| 1008 |
+
return f"{prefix} {remainder}"
|
| 1009 |
+
|
| 1010 |
+
# 2. Try all positions - STRICT: BOTH parts must be IV AND both >= 3 chars
|
| 1011 |
+
for i in range(3, len(word) - 2): # Both parts at least 3 chars
|
| 1012 |
+
left = word[:i]
|
| 1013 |
+
right = word[i:]
|
| 1014 |
+
|
| 1015 |
+
if self.vocab.is_iv(left) and self.vocab.is_iv(right):
|
| 1016 |
+
return f"{left} {right}"
|
| 1017 |
+
|
| 1018 |
+
# No valid split found
|
| 1019 |
+
return word
|
| 1020 |
+
|
| 1021 |
+
# Common Arabic pronoun/possessive suffixes (2-3 chars)
|
| 1022 |
+
# These are often incorrectly split from their host word
|
| 1023 |
+
PRONOUN_SUFFIXES = {'كم', 'هم', 'ها', 'هن', 'كن', 'نا', 'هما', 'كما', 'تم', 'تن'}
|
| 1024 |
+
|
| 1025 |
+
def merge_fragments(self, text: str) -> str:
|
| 1026 |
+
"""
|
| 1027 |
+
Try to merge adjacent OOV fragments into IV words.
|
| 1028 |
+
|
| 1029 |
+
Key patterns:
|
| 1030 |
+
1. Ta-marbuta detachment: السوري ة → السورية
|
| 1031 |
+
2. Al- detachment: ال كتاب → الكتاب
|
| 1032 |
+
3. General OOV+OOV merging: Only if both are OOV and result is IV
|
| 1033 |
+
4. Short OOV fragment: 1-2 char OOV + next → IV
|
| 1034 |
+
5. Pronoun suffix reattachment: علي كم → عليكم
|
| 1035 |
+
"""
|
| 1036 |
+
words = text.split()
|
| 1037 |
+
if len(words) < 2:
|
| 1038 |
+
return text
|
| 1039 |
+
|
| 1040 |
+
result = []
|
| 1041 |
+
i = 0
|
| 1042 |
+
|
| 1043 |
+
while i < len(words):
|
| 1044 |
+
word = words[i]
|
| 1045 |
+
|
| 1046 |
+
# Try to merge with next word
|
| 1047 |
+
if i + 1 < len(words):
|
| 1048 |
+
next_word = words[i + 1]
|
| 1049 |
+
merged = word + next_word
|
| 1050 |
+
|
| 1051 |
+
# Pattern 1: Detached suffix (ة، ه، ي، ك...)
|
| 1052 |
+
# Allow merging even if 'word' is IV because detached suffix is definitely wrong
|
| 1053 |
+
if len(next_word) == 1 and next_word in 'ةهاي':
|
| 1054 |
+
if self.vocab.is_iv(merged):
|
| 1055 |
+
result.append(merged)
|
| 1056 |
+
i += 2
|
| 1057 |
+
continue
|
| 1058 |
+
|
| 1059 |
+
# Pattern 2: Detached 'Al-' prefix
|
| 1060 |
+
# ال كتاب → الكتاب (Safe to merge)
|
| 1061 |
+
if word == 'ال' and len(next_word) >= 2:
|
| 1062 |
+
if self.vocab.is_iv(merged):
|
| 1063 |
+
result.append(merged)
|
| 1064 |
+
i += 2
|
| 1065 |
+
continue
|
| 1066 |
+
|
| 1067 |
+
# Pattern 3: General OOV + OOV → IV
|
| 1068 |
+
# STRICT: Both must be OOV to avoid merging valid words
|
| 1069 |
+
if self.vocab.is_oov(word) and self.vocab.is_oov(next_word):
|
| 1070 |
+
if self.vocab.is_iv(merged):
|
| 1071 |
+
result.append(merged)
|
| 1072 |
+
i += 2
|
| 1073 |
+
continue
|
| 1074 |
+
|
| 1075 |
+
# Pattern 4: Short OOV fragment (1-2 chars) merge
|
| 1076 |
+
if len(word) <= 2 and self.vocab.is_oov(word):
|
| 1077 |
+
if self.vocab.is_iv(merged):
|
| 1078 |
+
result.append(merged)
|
| 1079 |
+
i += 2
|
| 1080 |
+
continue
|
| 1081 |
+
|
| 1082 |
+
# Pattern 5: Pronoun suffix reattachment
|
| 1083 |
+
# Fixes over-splitting: علي كم → عليكم
|
| 1084 |
+
if next_word in self.PRONOUN_SUFFIXES:
|
| 1085 |
+
if self.vocab.is_iv(merged) and not self.vocab.is_iv(word):
|
| 1086 |
+
result.append(merged)
|
| 1087 |
+
i += 2
|
| 1088 |
+
continue
|
| 1089 |
+
|
| 1090 |
+
# Pattern 6: Short fragment merge
|
| 1091 |
+
# Merges two short words when combined they form a valid longer word
|
| 1092 |
+
# Fixes: علي كم → عليكم, ويت أمل → ويتأمل, المد فتر → المدفتر
|
| 1093 |
+
# Condition: both words ≤ 3 chars, merged ≥ 5 chars and IV
|
| 1094 |
+
if len(word) <= 3 and len(next_word) <= 3:
|
| 1095 |
+
if len(merged) >= 5 and self.vocab.is_iv(merged):
|
| 1096 |
+
result.append(merged)
|
| 1097 |
+
i += 2
|
| 1098 |
+
continue
|
| 1099 |
+
|
| 1100 |
+
result.append(word)
|
| 1101 |
+
i += 1
|
| 1102 |
+
|
| 1103 |
+
return ' '.join(result)
|
| 1104 |
+
|
| 1105 |
+
def process_text(self, text: str) -> str:
|
| 1106 |
+
"""
|
| 1107 |
+
Apply full split/merge processing to text.
|
| 1108 |
+
Order: First merge, then split.
|
| 1109 |
+
"""
|
| 1110 |
+
# Step 1: Merge fragments
|
| 1111 |
+
text = self.merge_fragments(text)
|
| 1112 |
+
|
| 1113 |
+
# Step 2: Split OOV words
|
| 1114 |
+
words = text.split()
|
| 1115 |
+
processed = []
|
| 1116 |
+
|
| 1117 |
+
for word in words:
|
| 1118 |
+
if self.vocab.is_oov(word) and len(word) >= 4:
|
| 1119 |
+
split_result = self.split_word(word)
|
| 1120 |
+
processed.append(split_result)
|
| 1121 |
+
else:
|
| 1122 |
+
processed.append(word)
|
| 1123 |
+
|
| 1124 |
+
return ' '.join(processed)
|
| 1125 |
+
|
| 1126 |
+
# ═══════════════════════════════════════════════════════════════════════════════
|
| 1127 |
+
# EDIT DISTANCE CORRECTOR
|
| 1128 |
+
# ═══════════════════════════════════════════════════════════════════════════════
|
| 1129 |
+
|
| 1130 |
+
class EditDistanceCorrector:
|
| 1131 |
+
"""
|
| 1132 |
+
Generates candidates based on Levenshtein distance.
|
| 1133 |
+
Uses BERT Vocabulary to filter for valid words.
|
| 1134 |
+
"""
|
| 1135 |
+
def __init__(self, tokenizer):
|
| 1136 |
+
self.tokenizer = tokenizer
|
| 1137 |
+
# Build strict vocabulary (ignore subwords starting with ## and punctuation)
|
| 1138 |
+
self.vocab = {
|
| 1139 |
+
w for w in tokenizer.get_vocab().keys()
|
| 1140 |
+
if w.isalpha() and not w.startswith('##') and len(w) > 1
|
| 1141 |
+
}
|
| 1142 |
+
# Frequency rank heuristic: lower index = higher frequency (usually)
|
| 1143 |
+
self.vocab_rank = {w: i for w, i in tokenizer.get_vocab().items()}
|
| 1144 |
+
|
| 1145 |
+
def edits1(self, word):
|
| 1146 |
+
"""All edits that are one edit away from `word`."""
|
| 1147 |
+
letters = 'أابتثجحخدذرزسشصضطظعغفقكلمنهويءآىةئؤ' # Arabic chars
|
| 1148 |
+
splits = [(word[:i], word[i:]) for i in range(len(word) + 1)]
|
| 1149 |
+
deletes = [L + R[1:] for L, R in splits if R]
|
| 1150 |
+
transposes = [L + R[1] + R[0] + R[2:] for L, R in splits if len(R)>1]
|
| 1151 |
+
replaces = [L + c + R[1:] for L, R in splits if R for c in letters]
|
| 1152 |
+
inserts = [L + c + R for L, R in splits for c in letters]
|
| 1153 |
+
return set(deletes + transposes + replaces + inserts)
|
| 1154 |
+
|
| 1155 |
+
def edits2(self, word):
|
| 1156 |
+
"""All edits that are two edits away from `word`."""
|
| 1157 |
+
return (e2 for e1 in self.edits1(word) for e2 in self.edits1(e1))
|
| 1158 |
+
|
| 1159 |
+
def known(self, words):
|
| 1160 |
+
"""The subset of `words` that appear in the dictionary of known words."""
|
| 1161 |
+
return set(w for w in words if w in self.vocab)
|
| 1162 |
+
|
| 1163 |
+
def generate_candidate(self, text: str) -> str:
|
| 1164 |
+
"""
|
| 1165 |
+
Generate a candidate sentence by fixing OOV words using Edit Distance.
|
| 1166 |
+
"""
|
| 1167 |
+
words = text.split()
|
| 1168 |
+
corrected_words = []
|
| 1169 |
+
|
| 1170 |
+
for word in words:
|
| 1171 |
+
# Clean word for checking
|
| 1172 |
+
clean_word = re.sub(r'[^\w]', '', word)
|
| 1173 |
+
|
| 1174 |
+
# If word is known, keep it
|
| 1175 |
+
if clean_word in self.vocab:
|
| 1176 |
+
corrected_words.append(word)
|
| 1177 |
+
continue
|
| 1178 |
+
|
| 1179 |
+
# If OOV, try to find neighbor
|
| 1180 |
+
# 1. Edits 1
|
| 1181 |
+
candidates = self.known(self.edits1(clean_word))
|
| 1182 |
+
|
| 1183 |
+
# 2. Edits 2 (if no Edits 1)
|
| 1184 |
+
if not candidates:
|
| 1185 |
+
# Optimize: Only check edits2 if word length is reasonable
|
| 1186 |
+
if len(clean_word) < 7:
|
| 1187 |
+
candidates = self.known(self.edits2(clean_word))
|
| 1188 |
+
|
| 1189 |
+
if candidates:
|
| 1190 |
+
# Pick best candidate: Lowest vocab rank (most frequent)
|
| 1191 |
+
best_candidate = min(candidates, key=lambda w: self.vocab_rank.get(w, 999999))
|
| 1192 |
+
corrected_words.append(best_candidate)
|
| 1193 |
+
else:
|
| 1194 |
+
# No correction found, keep original
|
| 1195 |
+
corrected_words.append(word)
|
| 1196 |
+
|
| 1197 |
+
return ' '.join(corrected_words)
|
| 1198 |
+
|
| 1199 |
+
|
| 1200 |
+
|
| 1201 |
+
|
| 1202 |
+
|
| 1203 |
+
|
| 1204 |
+
|
| 1205 |
+
# ═══════════════════════════════════════════════════════════════════════════════
|
| 1206 |
+
# CONTEXTUAL CORRECTOR (MLM-based with Batch Scoring)
|
| 1207 |
+
# ════════════════════════���══════════════════════════════════════════════════════
|
| 1208 |
+
|
| 1209 |
+
class ContextualCorrector:
|
| 1210 |
+
"""MLM-based contextual correction for confusion pairs"""
|
| 1211 |
+
|
| 1212 |
+
# Common confusion pairs in Arabic
|
| 1213 |
+
CONFUSION_PAIRS = [
|
| 1214 |
+
('ض', 'ظ'), ('ذ', 'ز'), ('ث', 'س'), ('ص', 'س'),
|
| 1215 |
+
('ط', 'ت'), ('ق', 'ك'), ('ه', 'ة'), ('ا', 'ى'),
|
| 1216 |
+
('ت', 'د'), ('د', 'ض'), ('ك', 'ق'), ('غ', 'ق'),
|
| 1217 |
+
('ج', 'ش'), ('س', 'ز'), ('ف', 'ب'), ('و', 'و'), # (و, و) placeholder, maybe (و, ؤ)?
|
| 1218 |
+
('ؤ', 'و'), ('ئ', 'ي'), ('ء', 'أ'), ('إ', 'أ'),
|
| 1219 |
+
]
|
| 1220 |
+
|
| 1221 |
+
def __init__(self, model_name: str = 'aubmindlab/bert-base-arabertv02', cache_size: int = 10000):
|
| 1222 |
+
"""Initialize with BERT MLM model and LRU cache"""
|
| 1223 |
+
from transformers import AutoTokenizer, AutoModelForMaskedLM
|
| 1224 |
+
from functools import lru_cache
|
| 1225 |
+
|
| 1226 |
+
self.tokenizer = AutoTokenizer.from_pretrained(model_name)
|
| 1227 |
+
self.model = AutoModelForMaskedLM.from_pretrained(model_name)
|
| 1228 |
+
self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
| 1229 |
+
self.model = self.model.to(self.device)
|
| 1230 |
+
self.model.eval()
|
| 1231 |
+
|
| 1232 |
+
# Build confusion map
|
| 1233 |
+
self.confusion_map = self._build_confusion_map()
|
| 1234 |
+
|
| 1235 |
+
# Stats
|
| 1236 |
+
self.cache_hits = 0
|
| 1237 |
+
self.cache_misses = 0
|
| 1238 |
+
|
| 1239 |
+
# Create LRU cache for scoring
|
| 1240 |
+
self._score_cache = {}
|
| 1241 |
+
self.cache_size = cache_size
|
| 1242 |
+
|
| 1243 |
+
# Load vocabulary for filtering
|
| 1244 |
+
self.vocab = self.tokenizer.get_vocab()
|
| 1245 |
+
|
| 1246 |
+
def _build_confusion_map(self):
|
| 1247 |
+
"""Build bidirectional confusion map"""
|
| 1248 |
+
confusion_map = {}
|
| 1249 |
+
for char1, char2 in self.CONFUSION_PAIRS:
|
| 1250 |
+
if char1 not in confusion_map:
|
| 1251 |
+
confusion_map[char1] = []
|
| 1252 |
+
if char2 not in confusion_map:
|
| 1253 |
+
confusion_map[char2] = []
|
| 1254 |
+
confusion_map[char1].append(char2)
|
| 1255 |
+
confusion_map[char2].append(char1)
|
| 1256 |
+
return confusion_map
|
| 1257 |
+
|
| 1258 |
+
def get_confusable_chars(self, char: str) -> List[str]:
|
| 1259 |
+
"""Get confusable characters for a given char"""
|
| 1260 |
+
return self.confusion_map.get(char, [])
|
| 1261 |
+
|
| 1262 |
+
def generate_candidates(self, word: str) -> List[str]:
|
| 1263 |
+
"""Generate candidate corrections for a word"""
|
| 1264 |
+
candidates = [word]
|
| 1265 |
+
|
| 1266 |
+
# 1. Substitute confusable chars
|
| 1267 |
+
for i, char in enumerate(word):
|
| 1268 |
+
confusables = self.get_confusable_chars(char)
|
| 1269 |
+
for conf_char in confusables:
|
| 1270 |
+
candidate = word[:i] + conf_char + word[i+1:]
|
| 1271 |
+
if candidate not in candidates:
|
| 1272 |
+
candidates.append(candidate)
|
| 1273 |
+
|
| 1274 |
+
# 2. Remove repeated characters (deletion)
|
| 1275 |
+
# Fixes: مدررسة -> مدرسة, جميلل -> جميل
|
| 1276 |
+
for i in range(len(word) - 1):
|
| 1277 |
+
if word[i] == word[i+1]:
|
| 1278 |
+
# Remove one instance of the repeated char
|
| 1279 |
+
candidate = word[:i] + word[i+1:]
|
| 1280 |
+
if candidate not in candidates:
|
| 1281 |
+
candidates.append(candidate)
|
| 1282 |
+
|
| 1283 |
+
# 3. Edit Distance 1 Candidates (Insertions, Substitutions, Transpositions)
|
| 1284 |
+
# Using a restricted set of characters to avoid explosion
|
| 1285 |
+
COMMON_CHARS = 'ابتثجحخدذرزسشصضطظعغفقكلمنهويأإآءئؤةى'
|
| 1286 |
+
|
| 1287 |
+
# Filter candidates by vocabulary to prevent hallucinations and scoring errors
|
| 1288 |
+
# Only keep candidates that are valid single tokens in the vocabulary.
|
| 1289 |
+
|
| 1290 |
+
# Insertions (missing char)
|
| 1291 |
+
for i in range(len(word) + 1):
|
| 1292 |
+
for char in COMMON_CHARS:
|
| 1293 |
+
candidate = word[:i] + char + word[i:]
|
| 1294 |
+
if candidate in self.vocab and candidate not in candidates:
|
| 1295 |
+
candidates.append(candidate)
|
| 1296 |
+
|
| 1297 |
+
# Substitutions (wrong char)
|
| 1298 |
+
if len(word) < 7:
|
| 1299 |
+
for i in range(len(word)):
|
| 1300 |
+
for char in COMMON_CHARS:
|
| 1301 |
+
if char != word[i]:
|
| 1302 |
+
candidate = word[:i] + char + word[i+1:]
|
| 1303 |
+
if candidate in self.vocab and candidate not in candidates:
|
| 1304 |
+
candidates.append(candidate)
|
| 1305 |
+
|
| 1306 |
+
# Deletions (extra char) - General
|
| 1307 |
+
for i in range(len(word)):
|
| 1308 |
+
candidate = word[:i] + word[i+1:]
|
| 1309 |
+
if len(candidate) > 1:
|
| 1310 |
+
# For deletions, candidate might be a valid word even if not in vocab?
|
| 1311 |
+
# But to be safe and consistent with scoring, let's enforce vocab.
|
| 1312 |
+
# (Note: 'جميل' IS in vocab, so it works).
|
| 1313 |
+
if candidate in self.vocab and candidate not in candidates:
|
| 1314 |
+
candidates.append(candidate)
|
| 1315 |
+
|
| 1316 |
+
return candidates
|
| 1317 |
+
|
| 1318 |
+
def score_with_mlm(self, text: str, position: int, word: str) -> float:
|
| 1319 |
+
"""Score a word in context using BERT MLM"""
|
| 1320 |
+
# Check cache
|
| 1321 |
+
cache_key = f"{text}|{position}|{word}"
|
| 1322 |
+
if cache_key in self._score_cache:
|
| 1323 |
+
self.cache_hits += 1
|
| 1324 |
+
return self._score_cache[cache_key]
|
| 1325 |
+
|
| 1326 |
+
self.cache_misses += 1
|
| 1327 |
+
|
| 1328 |
+
# Create masked text
|
| 1329 |
+
words = text.split()
|
| 1330 |
+
if position >= len(words):
|
| 1331 |
+
return 0.0
|
| 1332 |
+
|
| 1333 |
+
masked_words = words.copy()
|
| 1334 |
+
masked_words[position] = '[MASK]'
|
| 1335 |
+
masked_text = ' '.join(masked_words)
|
| 1336 |
+
|
| 1337 |
+
# Tokenize
|
| 1338 |
+
inputs = self.tokenizer(masked_text, return_tensors='pt', padding=True, truncation=True)
|
| 1339 |
+
inputs = {k: v.to(self.device) for k, v in inputs.items()}
|
| 1340 |
+
|
| 1341 |
+
# Get predictions
|
| 1342 |
+
with torch.no_grad():
|
| 1343 |
+
outputs = self.model(**inputs)
|
| 1344 |
+
predictions = outputs.logits
|
| 1345 |
+
|
| 1346 |
+
# Find mask position
|
| 1347 |
+
mask_token_index = (inputs['input_ids'] == self.tokenizer.mask_token_id).nonzero(as_tuple=True)[1]
|
| 1348 |
+
|
| 1349 |
+
if len(mask_token_index) == 0:
|
| 1350 |
+
return 0.0
|
| 1351 |
+
|
| 1352 |
+
# Get probabilities for the word
|
| 1353 |
+
mask_token_logits = predictions[0, mask_token_index[0], :]
|
| 1354 |
+
probs = torch.softmax(mask_token_logits, dim=0)
|
| 1355 |
+
|
| 1356 |
+
# Get word token id
|
| 1357 |
+
word_tokens = self.tokenizer.encode(word, add_special_tokens=False)
|
| 1358 |
+
if not word_tokens:
|
| 1359 |
+
return 0.0
|
| 1360 |
+
|
| 1361 |
+
word_token_id = word_tokens[0]
|
| 1362 |
+
score = probs[word_token_id].item()
|
| 1363 |
+
|
| 1364 |
+
# Update cache (with size limit)
|
| 1365 |
+
if len(self._score_cache) >= self.cache_size:
|
| 1366 |
+
# Remove oldest entry (simple FIFO)
|
| 1367 |
+
self._score_cache.pop(next(iter(self._score_cache)))
|
| 1368 |
+
|
| 1369 |
+
self._score_cache[cache_key] = score
|
| 1370 |
+
|
| 1371 |
+
return score
|
| 1372 |
+
|
| 1373 |
+
def score_candidates_batch(self, text: str, position: int, candidates: List[str]) -> dict:
|
| 1374 |
+
"""
|
| 1375 |
+
Batch score multiple candidates (NEW - more efficient!)
|
| 1376 |
+
Returns: {candidate: score}
|
| 1377 |
+
"""
|
| 1378 |
+
scores = {}
|
| 1379 |
+
|
| 1380 |
+
for candidate in candidates:
|
| 1381 |
+
scores[candidate] = self.score_with_mlm(text, position, candidate)
|
| 1382 |
+
|
| 1383 |
+
return scores
|
| 1384 |
+
|
| 1385 |
+
def predict_masked_token(self, text: str, position: int, top_k: int = 5) -> List[Tuple[str, float]]:
|
| 1386 |
+
"""Predict words for a masked position. Returns list of (word, score)."""
|
| 1387 |
+
words = text.split()
|
| 1388 |
+
if position >= len(words):
|
| 1389 |
+
return []
|
| 1390 |
+
|
| 1391 |
+
masked_words = words.copy()
|
| 1392 |
+
masked_words[position] = '[MASK]'
|
| 1393 |
+
masked_text = ' '.join(masked_words)
|
| 1394 |
+
|
| 1395 |
+
inputs = self.tokenizer(masked_text, return_tensors='pt', padding=True, truncation=True).to(self.device)
|
| 1396 |
+
|
| 1397 |
+
with torch.no_grad():
|
| 1398 |
+
outputs = self.model(**inputs)
|
| 1399 |
+
predictions = outputs.logits
|
| 1400 |
+
|
| 1401 |
+
mask_token_index = (inputs['input_ids'] == self.tokenizer.mask_token_id).nonzero(as_tuple=True)[1]
|
| 1402 |
+
|
| 1403 |
+
if len(mask_token_index) == 0:
|
| 1404 |
+
return []
|
| 1405 |
+
|
| 1406 |
+
mask_token_logits = predictions[0, mask_token_index[0], :]
|
| 1407 |
+
probs = torch.softmax(mask_token_logits, dim=0)
|
| 1408 |
+
|
| 1409 |
+
top_k_weights, top_k_indices = torch.topk(probs, top_k, sorted=True)
|
| 1410 |
+
|
| 1411 |
+
results = []
|
| 1412 |
+
for i in range(top_k):
|
| 1413 |
+
token_id = top_k_indices[i].item()
|
| 1414 |
+
score = top_k_weights[i].item()
|
| 1415 |
+
token = self.tokenizer.decode([token_id]).strip()
|
| 1416 |
+
if not token.startswith("##") and token not in self.tokenizer.all_special_tokens:
|
| 1417 |
+
results.append((token, score))
|
| 1418 |
+
|
| 1419 |
+
return results
|
| 1420 |
+
|
| 1421 |
+
def refine_sentence_with_mask(self, text: str, threshold: float = 0.001, vocab_manager=None, raw_model_output=None) -> str:
|
| 1422 |
+
"""Refine sentence by masking weak words and predicting replacements.
|
| 1423 |
+
IV-Safe + Strict similarity + BERT Kill Switch.
|
| 1424 |
+
"""
|
| 1425 |
+
words = text.split()
|
| 1426 |
+
refined_words = words.copy()
|
| 1427 |
+
|
| 1428 |
+
# Build set of raw model words for kill switch
|
| 1429 |
+
raw_words = raw_model_output.split() if raw_model_output else []
|
| 1430 |
+
|
| 1431 |
+
for i, word in enumerate(words):
|
| 1432 |
+
# IV-Safe check - NEVER replace IV words
|
| 1433 |
+
if vocab_manager and vocab_manager.is_iv(word):
|
| 1434 |
+
continue
|
| 1435 |
+
|
| 1436 |
+
# BERT Kill Switch: skip words matching raw model output
|
| 1437 |
+
if i < len(raw_words) and word == raw_words[i]:
|
| 1438 |
+
continue
|
| 1439 |
+
|
| 1440 |
+
# Skip very short words (prepositions etc)
|
| 1441 |
+
if len(word) <= 2:
|
| 1442 |
+
continue
|
| 1443 |
+
|
| 1444 |
+
# 1. Check confidence
|
| 1445 |
+
current_score = self.score_with_mlm(text, i, word)
|
| 1446 |
+
|
| 1447 |
+
if current_score > threshold:
|
| 1448 |
+
continue
|
| 1449 |
+
|
| 1450 |
+
# 2. Mask and Predict
|
| 1451 |
+
predictions = self.predict_masked_token(text, i, top_k=10)
|
| 1452 |
+
|
| 1453 |
+
# 3. Filter and Select (strict)
|
| 1454 |
+
for pred_word, pred_score in predictions:
|
| 1455 |
+
if pred_word == word:
|
| 1456 |
+
continue
|
| 1457 |
+
|
| 1458 |
+
if abs(len(pred_word) - len(word)) > 1:
|
| 1459 |
+
continue
|
| 1460 |
+
|
| 1461 |
+
# Similarity Check (0.90 minimum)
|
| 1462 |
+
dist = Levenshtein.distance(word, pred_word)
|
| 1463 |
+
max_len = max(len(word), len(pred_word))
|
| 1464 |
+
similarity = 1.0 - (dist / max_len)
|
| 1465 |
+
|
| 1466 |
+
if similarity < 0.90:
|
| 1467 |
+
continue
|
| 1468 |
+
|
| 1469 |
+
# Must be IV
|
| 1470 |
+
if vocab_manager and vocab_manager.is_oov(pred_word):
|
| 1471 |
+
continue
|
| 1472 |
+
|
| 1473 |
+
# Minimum absolute confidence gate (12%)
|
| 1474 |
+
if pred_score < 0.12:
|
| 1475 |
+
continue
|
| 1476 |
+
|
| 1477 |
+
# Score Improvement
|
| 1478 |
+
is_original_common = current_score > 0.001
|
| 1479 |
+
|
| 1480 |
+
if is_original_common:
|
| 1481 |
+
if pred_score > current_score * 1000:
|
| 1482 |
+
refined_words[i] = pred_word
|
| 1483 |
+
break
|
| 1484 |
+
else:
|
| 1485 |
+
if pred_score > current_score * 50 and pred_score > 0.2:
|
| 1486 |
+
refined_words[i] = pred_word
|
| 1487 |
+
break
|
| 1488 |
+
|
| 1489 |
+
return ' '.join(refined_words)
|
| 1490 |
+
|
| 1491 |
+
def calculate_sentence_score(self, text: str) -> float:
|
| 1492 |
+
"""Calculate fluency score using BERT MLM average word probability."""
|
| 1493 |
+
words = text.split()
|
| 1494 |
+
if not words:
|
| 1495 |
+
return 0.0
|
| 1496 |
+
|
| 1497 |
+
total_score = 0.0
|
| 1498 |
+
scored_words = 0
|
| 1499 |
+
|
| 1500 |
+
for i, word in enumerate(words):
|
| 1501 |
+
score = self.score_with_mlm(text, i, word)
|
| 1502 |
+
total_score += score
|
| 1503 |
+
scored_words += 1
|
| 1504 |
+
|
| 1505 |
+
if scored_words == 0:
|
| 1506 |
+
return 0.0
|
| 1507 |
+
|
| 1508 |
+
return total_score / scored_words
|
| 1509 |
+
|
| 1510 |
+
|
| 1511 |
+
# ═══════════════════════════════════════════════════════════════════════════════
|
| 1512 |
+
# MAIN SPELL CHECKER CLASS
|
| 1513 |
+
# ═══════════════════════════════════════════════════════════════════════════════
|
| 1514 |
+
|
| 1515 |
+
class ArabicSpellChecker:
|
| 1516 |
+
"""Main Arabic Spell Checker class"""
|
| 1517 |
+
|
| 1518 |
+
def __init__(self, model, tokenizer, device, use_contextual: bool = True):
|
| 1519 |
+
"""Initialize spell checker with model and components"""
|
| 1520 |
+
self.model = model
|
| 1521 |
+
self.tokenizer = tokenizer
|
| 1522 |
+
self.device = device
|
| 1523 |
+
|
| 1524 |
+
# Initialize components
|
| 1525 |
+
self.postprocessor = AraSpellPostProcessor()
|
| 1526 |
+
self.classifier = ErrorClassifier()
|
| 1527 |
+
self.rules = RulesBasedCorrector()
|
| 1528 |
+
self.validator = OutputValidator()
|
| 1529 |
+
self.vocab_manager = VocabularyManager(tokenizer)
|
| 1530 |
+
self.edit_corrector = EditDistanceCorrector(tokenizer) # Edit Distance candidates
|
| 1531 |
+
self.split_merge = SplitMergeSpecialist(self.vocab_manager)
|
| 1532 |
+
|
| 1533 |
+
# WordAligner for word-level hybrid corrections
|
| 1534 |
+
self.word_aligner = WordAligner(self.vocab_manager)
|
| 1535 |
+
|
| 1536 |
+
# Initialize contextual corrector (optional)
|
| 1537 |
+
self.use_contextual = use_contextual
|
| 1538 |
+
if use_contextual:
|
| 1539 |
+
try:
|
| 1540 |
+
self.contextual = ContextualCorrector()
|
| 1541 |
+
logger.info("Contextual correction enabled")
|
| 1542 |
+
except Exception as e:
|
| 1543 |
+
logger.warning(f"Contextual correction disabled: {e}")
|
| 1544 |
+
self.contextual = None
|
| 1545 |
+
self.use_contextual = False
|
| 1546 |
+
else:
|
| 1547 |
+
self.contextual = None
|
| 1548 |
+
def _fix_repeated_end_chars(self, text: str) -> str:
|
| 1549 |
+
"""
|
| 1550 |
+
🆕 Fix repeated characters at word endings
|
| 1551 |
+
|
| 1552 |
+
Examples:
|
| 1553 |
+
اليومم → اليوم
|
| 1554 |
+
جميلل → جميل
|
| 1555 |
+
صباحح → صباح
|
| 1556 |
+
"""
|
| 1557 |
+
# Remove repeated chars at word end (keep only one)
|
| 1558 |
+
text = re.sub(r'([ا-ي])\1+\b', r'\1', text)
|
| 1559 |
+
return text
|
| 1560 |
+
|
| 1561 |
+
def _fix_merged_with_errors(self, text: str) -> str:
|
| 1562 |
+
""" Fix merged words that contain errors
|
| 1563 |
+
|
| 1564 |
+
Examples:
|
| 1565 |
+
الممدرسة → المدرسة
|
| 1566 |
+
الكتابب → الكتاب
|
| 1567 |
+
الططالب → الطالب
|
| 1568 |
+
"""
|
| 1569 |
+
# Pattern 1: ال + repeated char + word
|
| 1570 |
+
text = re.sub(r'ال([ا-ي])\1+([ا-ي]{2,})', r'ال\2', text)
|
| 1571 |
+
|
| 1572 |
+
# Pattern 2: word + repeated char at end
|
| 1573 |
+
text = re.sub(r'\b([ا-ي]{3,})([ا-ي])\2+\b', r'\1\2', text)
|
| 1574 |
+
|
| 1575 |
+
return text
|
| 1576 |
+
|
| 1577 |
+
|
| 1578 |
+
def _split_merged_words_linguistic(self, text: str) -> str:
|
| 1579 |
+
""" Split merged words using linguistic patterns
|
| 1580 |
+
|
| 1581 |
+
Examples:
|
| 1582 |
+
كلصباح → كل صباح
|
| 1583 |
+
فيالطريق → في الطريق
|
| 1584 |
+
السلامعليكم → السلام عليكم
|
| 1585 |
+
"""
|
| 1586 |
+
# Pattern 1: Prepositions + (article)? + word
|
| 1587 |
+
# Added: ك (like in كالكتاب) but careful not to split overlapping words
|
| 1588 |
+
text = re.sub(
|
| 1589 |
+
r'\b(في|من|إلى|الى|حتى|منذ|خلال|بعد|قبل)(ال)?([ا-ي]{3,})',
|
| 1590 |
+
r'\1 \2\3',
|
| 1591 |
+
text
|
| 1592 |
+
)
|
| 1593 |
+
|
| 1594 |
+
# Pattern 2: كل + word
|
| 1595 |
+
text = re.sub(r'\b(كل)([ا-ي]{3,})', r'\1 \2', text)
|
| 1596 |
+
|
| 1597 |
+
# Pattern 3: Article repetition
|
| 1598 |
+
text = re.sub(r'([ا-ي]{3,})(ال)([ا-ي]{3,})', r'\1 \2\3', text)
|
| 1599 |
+
|
| 1600 |
+
# Pattern 4: Single-letter prepositions
|
| 1601 |
+
text = re.sub(r'\b([بلك])(ال)?([ا-ي]{3,})', r'\1 \2\3', text)
|
| 1602 |
+
|
| 1603 |
+
# Pattern 5: Word + عليكم/عليك
|
| 1604 |
+
text = re.sub(r'([ا-ي]{4,})(عليكم|عليك|عليه|عليها)', r'\1 \2', text)
|
| 1605 |
+
|
| 1606 |
+
# Pattern 6: على/عن in middle of (merged) words
|
| 1607 |
+
text = re.sub(r'([ا-ي]{3,})(على|عن)([ا-ي]{3,})', r'\1 \2 \3', text)
|
| 1608 |
+
|
| 1609 |
+
return text
|
| 1610 |
+
|
| 1611 |
+
def _split_long_words_heuristic(self, text: str, max_length: int = 15) -> str:
|
| 1612 |
+
""" Split suspiciously long words using heuristics
|
| 1613 |
+
"""
|
| 1614 |
+
words = text.split()
|
| 1615 |
+
result = []
|
| 1616 |
+
|
| 1617 |
+
for word in words:
|
| 1618 |
+
if len(word) <= max_length:
|
| 1619 |
+
result.append(word)
|
| 1620 |
+
continue
|
| 1621 |
+
|
| 1622 |
+
# Check for embedded article
|
| 1623 |
+
if 'ال' in word[2:]:
|
| 1624 |
+
parts = word.split('ال', 1)
|
| 1625 |
+
if len(parts[0]) >= 2 and len(parts[1]) >= 3:
|
| 1626 |
+
result.extend([parts[0], 'ال' + parts[1]])
|
| 1627 |
+
continue
|
| 1628 |
+
|
| 1629 |
+
# Check for common prefixes at start of long word
|
| 1630 |
+
if len(word) >= 8:
|
| 1631 |
+
split_found = False
|
| 1632 |
+
for split_pos in [2, 3]:
|
| 1633 |
+
prefix = word[:split_pos]
|
| 1634 |
+
suffix = word[split_pos:]
|
| 1635 |
+
|
| 1636 |
+
if prefix in ['في', 'من', 'على', 'عن', 'مع', 'كل', 'ب', 'ل', 'ك']:
|
| 1637 |
+
result.extend([prefix, suffix])
|
| 1638 |
+
split_found = True
|
| 1639 |
+
break
|
| 1640 |
+
|
| 1641 |
+
if not split_found:
|
| 1642 |
+
result.append(word)
|
| 1643 |
+
else:
|
| 1644 |
+
result.append(word)
|
| 1645 |
+
|
| 1646 |
+
return ' '.join(result)
|
| 1647 |
+
|
| 1648 |
+
def _normalize_tanween_patterns(self, text: str) -> str:
|
| 1649 |
+
""" Normalize tanween patterns
|
| 1650 |
+
|
| 1651 |
+
Examples:
|
| 1652 |
+
جدأ → جداً
|
| 1653 |
+
كثيرأ → كثيراً
|
| 1654 |
+
"""
|
| 1655 |
+
# أ at word end → اً
|
| 1656 |
+
text = re.sub(r'([ا-ي]{2,})أ\b', r'\1اً', text)
|
| 1657 |
+
|
| 1658 |
+
# Remove standalone أ
|
| 1659 |
+
text = re.sub(r'\s+أ\s+', ' ', text)
|
| 1660 |
+
|
| 1661 |
+
# Fix accidental splits (e.g. ب + space + word)
|
| 1662 |
+
text = re.sub(r'\b([بلك])\s+([ا-ي])', r'\1\2', text)
|
| 1663 |
+
|
| 1664 |
+
return text
|
| 1665 |
+
|
| 1666 |
+
|
| 1667 |
+
|
| 1668 |
+
|
| 1669 |
+
|
| 1670 |
+
def preprocess(self, text: str) -> str:
|
| 1671 |
+
"""Preprocessing pipeline (مع التحسينات المدمجة)"""
|
| 1672 |
+
# Basic normalization
|
| 1673 |
+
text = self.postprocessor.remove_harakat(text)
|
| 1674 |
+
text = self.postprocessor.remove_tatweel(text)
|
| 1675 |
+
text = self.postprocessor.normalize_special_chars(text)
|
| 1676 |
+
|
| 1677 |
+
# Integrated improvements
|
| 1678 |
+
# Fix repeated chars and merged words with errors FIRST
|
| 1679 |
+
text = self._fix_repeated_end_chars(text)
|
| 1680 |
+
text = self._fix_merged_with_errors(text)
|
| 1681 |
+
|
| 1682 |
+
# Then split merged words
|
| 1683 |
+
text = self._split_merged_words_linguistic(text)
|
| 1684 |
+
text = self._split_long_words_heuristic(text)
|
| 1685 |
+
text = self._normalize_tanween_patterns(text)
|
| 1686 |
+
|
| 1687 |
+
# Merge separated 'ال'
|
| 1688 |
+
text = self.postprocessor.merge_separated_al(text)
|
| 1689 |
+
|
| 1690 |
+
# Collapse repetitions
|
| 1691 |
+
text = self.postprocessor.unified_collapse_repeated(text)
|
| 1692 |
+
|
| 1693 |
+
# Rules-based fixes
|
| 1694 |
+
text = self.rules.fix_char_substitution(text)
|
| 1695 |
+
text = self.rules.fix_char_repetition(text)
|
| 1696 |
+
|
| 1697 |
+
# Normalize spaces
|
| 1698 |
+
text = self.postprocessor.normalize_spaces(text)
|
| 1699 |
+
|
| 1700 |
+
return text
|
| 1701 |
+
|
| 1702 |
+
def postprocess(self, text: str, original: str = "") -> str:
|
| 1703 |
+
"""Postprocessing pipeline — passes vocab_manager for smart ه/ة handling"""
|
| 1704 |
+
return self.postprocessor.full_postprocess(text, original, vocab_manager=self.vocab_manager)
|
| 1705 |
+
|
| 1706 |
+
def model_inference(self, text: str, num_return_sequences: int = 5) -> List[str]:
|
| 1707 |
+
"""Run seq2seq model inference and return top candidates.
|
| 1708 |
+
Also extracts beam scores (token-level probabilities) for diagnostics.
|
| 1709 |
+
"""
|
| 1710 |
+
# Tokenize
|
| 1711 |
+
inputs = self.tokenizer(text, return_tensors='pt', padding=True, truncation=True, max_length=128)
|
| 1712 |
+
inputs = {k: v.to(self.device) for k, v in inputs.items()}
|
| 1713 |
+
|
| 1714 |
+
# Generate with beam search
|
| 1715 |
+
# Keeping 5 beams as model was trained/optimized for this
|
| 1716 |
+
# Keeping 5 beams as model was trained/optimized for this
|
| 1717 |
+
with torch.no_grad():
|
| 1718 |
+
outputs = self.model.generate(
|
| 1719 |
+
**inputs,
|
| 1720 |
+
num_beams=5,
|
| 1721 |
+
num_return_sequences=num_return_sequences,
|
| 1722 |
+
early_stopping=True,
|
| 1723 |
+
return_dict_in_generate=True,
|
| 1724 |
+
output_scores=True
|
| 1725 |
+
)
|
| 1726 |
+
|
| 1727 |
+
# Decode
|
| 1728 |
+
candidates = self.tokenizer.batch_decode(outputs.sequences, skip_special_tokens=True)
|
| 1729 |
+
|
| 1730 |
+
# Store beam scores for potential use
|
| 1731 |
+
self._last_beam_scores = {}
|
| 1732 |
+
if hasattr(outputs, 'sequences_scores') and outputs.sequences_scores is not None:
|
| 1733 |
+
scores = outputs.sequences_scores.tolist()
|
| 1734 |
+
for cand, score in zip(candidates, scores):
|
| 1735 |
+
self._last_beam_scores[cand] = score
|
| 1736 |
+
|
| 1737 |
+
return candidates
|
| 1738 |
+
|
| 1739 |
+
def correct(self, text: str) -> str:
|
| 1740 |
+
"""
|
| 1741 |
+
Main correction pipeline (RERANKING APPROACH)
|
| 1742 |
+
|
| 1743 |
+
Steps:
|
| 1744 |
+
1. Preprocess
|
| 1745 |
+
2. Generate Candidates (Model Beams + Baseline)
|
| 1746 |
+
3. Rerank Candidates (Validator + Fluency)
|
| 1747 |
+
4. Select Best
|
| 1748 |
+
5. Postprocess
|
| 1749 |
+
"""
|
| 1750 |
+
if not text or not text.strip():
|
| 1751 |
+
return text
|
| 1752 |
+
|
| 1753 |
+
original = text
|
| 1754 |
+
|
| 1755 |
+
# 1. Preprocess
|
| 1756 |
+
# This provides a strong baseline candidate
|
| 1757 |
+
preprocessed_text = self.preprocess(text)
|
| 1758 |
+
|
| 1759 |
+
# 2. Classify error type
|
| 1760 |
+
error_type = self.classifier.classify(preprocessed_text)
|
| 1761 |
+
|
| 1762 |
+
# 3. Generate Candidates
|
| 1763 |
+
candidates = []
|
| 1764 |
+
|
| 1765 |
+
# A. Baseline (Preprocessed)
|
| 1766 |
+
candidates.append(preprocessed_text)
|
| 1767 |
+
|
| 1768 |
+
# B. Smart Rules Candidate (Aggressive Heuristic)
|
| 1769 |
+
rules_candidate = self.rules.advanced_heuristic_repair(text)
|
| 1770 |
+
candidates.append(rules_candidate)
|
| 1771 |
+
|
| 1772 |
+
# B2. Edit Distance Candidate
|
| 1773 |
+
edit_candidate = self.edit_corrector.generate_candidate(text)
|
| 1774 |
+
if edit_candidate != text and edit_candidate != rules_candidate:
|
| 1775 |
+
candidates.append(edit_candidate)
|
| 1776 |
+
|
| 1777 |
+
# C. Model Beams
|
| 1778 |
+
raw_model_output = None # Track for safety net
|
| 1779 |
+
try:
|
| 1780 |
+
model_candidates = self.model_inference(preprocessed_text, num_return_sequences=5)
|
| 1781 |
+
raw_model_output = model_candidates[0] if model_candidates else None
|
| 1782 |
+
candidates.extend(model_candidates)
|
| 1783 |
+
|
| 1784 |
+
# D. Word-Aligned Hybrid Candidate
|
| 1785 |
+
# Creates a hybrid by selecting best word from each position
|
| 1786 |
+
if model_candidates:
|
| 1787 |
+
hybrid_candidate = self.word_aligner.align_words(preprocessed_text, model_candidates[0])
|
| 1788 |
+
if hybrid_candidate not in candidates:
|
| 1789 |
+
candidates.append(hybrid_candidate)
|
| 1790 |
+
|
| 1791 |
+
# E. Word-Aligned with ALL top beams (not just beam 0)
|
| 1792 |
+
for beam in model_candidates[1:3]: # Top 3 beams
|
| 1793 |
+
hybrid_beam = self.word_aligner.align_words(preprocessed_text, beam)
|
| 1794 |
+
if hybrid_beam not in candidates:
|
| 1795 |
+
candidates.append(hybrid_beam)
|
| 1796 |
+
|
| 1797 |
+
# D2. Token-level Voting Candidate
|
| 1798 |
+
# Majority-vote each token across all beams
|
| 1799 |
+
if model_candidates and len(model_candidates) >= 3:
|
| 1800 |
+
try:
|
| 1801 |
+
beam_word_lists = [c.split() for c in model_candidates]
|
| 1802 |
+
max_words = max(len(wl) for wl in beam_word_lists)
|
| 1803 |
+
voted_words = []
|
| 1804 |
+
for pos in range(max_words):
|
| 1805 |
+
words_at_pos = []
|
| 1806 |
+
for wl in beam_word_lists:
|
| 1807 |
+
if pos < len(wl):
|
| 1808 |
+
words_at_pos.append(wl[pos])
|
| 1809 |
+
if words_at_pos:
|
| 1810 |
+
most_common = Counter(words_at_pos).most_common(1)[0][0]
|
| 1811 |
+
voted_words.append(most_common)
|
| 1812 |
+
voted_candidate = ' '.join(voted_words)
|
| 1813 |
+
if voted_candidate not in candidates:
|
| 1814 |
+
candidates.append(voted_candidate)
|
| 1815 |
+
except Exception:
|
| 1816 |
+
pass
|
| 1817 |
+
except Exception as e:
|
| 1818 |
+
logger.warning(f"Model inference failed: {e}")
|
| 1819 |
+
|
| 1820 |
+
# Remove duplicates while preserving order
|
| 1821 |
+
unique_candidates = []
|
| 1822 |
+
seen = set()
|
| 1823 |
+
for c in candidates:
|
| 1824 |
+
if c not in seen:
|
| 1825 |
+
unique_candidates.append(c)
|
| 1826 |
+
seen.add(c)
|
| 1827 |
+
candidates = unique_candidates
|
| 1828 |
+
|
| 1829 |
+
|
| 1830 |
+
|
| 1831 |
+
# 4. Rerank Candidates
|
| 1832 |
+
best_candidate = preprocessed_text
|
| 1833 |
+
best_score = -1.0
|
| 1834 |
+
|
| 1835 |
+
# Debug info
|
| 1836 |
+
candidate_scores = []
|
| 1837 |
+
|
| 1838 |
+
for cand in candidates:
|
| 1839 |
+
# A. Validation Score (Hard Penalty)
|
| 1840 |
+
# Check validity against strict original
|
| 1841 |
+
is_valid, reason = self.validator.validate(original, cand, error_type.value)
|
| 1842 |
+
|
| 1843 |
+
# Additional check: If candidate is suspiciously shorter than original (and not just harakat removal)
|
| 1844 |
+
if len(cand) < len(original) * 0.5:
|
| 1845 |
+
is_valid = False
|
| 1846 |
+
reason = "too_short"
|
| 1847 |
+
|
| 1848 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 1849 |
+
# VOCABULARY-AWARE ACCEPTANCE
|
| 1850 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 1851 |
+
# Logic: OOV→IV = ACCEPT (boost), IV→OOV = REJECT (penalize)
|
| 1852 |
+
# This prevents over-conservative validation from rejecting correct corrections
|
| 1853 |
+
|
| 1854 |
+
input_oov_count = self.vocab_manager.count_oov_words(original)
|
| 1855 |
+
cand_oov_count = self.vocab_manager.count_oov_words(cand)
|
| 1856 |
+
|
| 1857 |
+
vocab_boost = 1.0
|
| 1858 |
+
|
| 1859 |
+
# Case 1: OOV→IV (Correction fixed unknown words) → Accept more readily
|
| 1860 |
+
if input_oov_count > 0 and cand_oov_count < input_oov_count:
|
| 1861 |
+
# Significant boost for reducing OOV words
|
| 1862 |
+
oov_reduction = input_oov_count - cand_oov_count
|
| 1863 |
+
vocab_boost = 1.0 + (oov_reduction * 0.3) # +30% per OOV fixed
|
| 1864 |
+
|
| 1865 |
+
# If ALL words are now IV, accept even with higher edit distance
|
| 1866 |
+
if cand_oov_count == 0 and self.vocab_manager.all_words_iv(cand):
|
| 1867 |
+
# Override validation rejection if OOV→IV
|
| 1868 |
+
if not is_valid and reason not in ["empty_output"]:
|
| 1869 |
+
is_valid = True
|
| 1870 |
+
reason = "vocab_aware_accept"
|
| 1871 |
+
|
| 1872 |
+
# Case 2: IV→OOV (Correction introduced unknown words) → Penalize
|
| 1873 |
+
elif cand_oov_count > input_oov_count:
|
| 1874 |
+
# Penalize for introducing new OOV words
|
| 1875 |
+
vocab_boost = 0.5 # 50% penalty
|
| 1876 |
+
|
| 1877 |
+
# Case 3: All IV to begin with → Standard validation
|
| 1878 |
+
elif input_oov_count == 0 and cand_oov_count == 0:
|
| 1879 |
+
# Both are valid vocab, prefer minimal edits
|
| 1880 |
+
vocab_boost = 1.0
|
| 1881 |
+
|
| 1882 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 1883 |
+
|
| 1884 |
+
|
| 1885 |
+
# Penalty factor
|
| 1886 |
+
# Valid: 1.0
|
| 1887 |
+
# Invalid: 0.01 (Heavy penalty, essentially disqualified unless all are invalid)
|
| 1888 |
+
validity_factor = 1.0 if is_valid else 0.001
|
| 1889 |
+
|
| 1890 |
+
# B. Fluency Score (BERT MLM)
|
| 1891 |
+
fluency_score = 0.0
|
| 1892 |
+
if self.use_contextual and self.contextual:
|
| 1893 |
+
try:
|
| 1894 |
+
fluency_score = self.contextual.calculate_sentence_score(cand)
|
| 1895 |
+
except Exception as e:
|
| 1896 |
+
logger.warning(f"Scoring failed: {e}")
|
| 1897 |
+
fluency_score = 0.5 # Default fallback
|
| 1898 |
+
else:
|
| 1899 |
+
fluency_score = 1.0
|
| 1900 |
+
|
| 1901 |
+
# C. Similarity Score (Damerau-Levenshtein Distance)
|
| 1902 |
+
dist = VocabularyManager.damerau_levenshtein_distance(preprocessed_text, cand)
|
| 1903 |
+
max_len = max(len(preprocessed_text), len(cand), 1)
|
| 1904 |
+
similarity = 1.0 - (dist / max_len)
|
| 1905 |
+
|
| 1906 |
+
# Boost exact matches
|
| 1907 |
+
if cand == preprocessed_text:
|
| 1908 |
+
similarity = 1.0
|
| 1909 |
+
|
| 1910 |
+
# Keyboard Proximity Bonus
|
| 1911 |
+
# If changes between input and candidate are keyboard-adjacent,
|
| 1912 |
+
# it's more likely a typo fix (give bonus)
|
| 1913 |
+
keyboard_bonus = 1.0
|
| 1914 |
+
input_words = preprocessed_text.split()
|
| 1915 |
+
cand_words = cand.split()
|
| 1916 |
+
if len(input_words) == len(cand_words):
|
| 1917 |
+
for iw, cw in zip(input_words, cand_words):
|
| 1918 |
+
if iw != cw and len(iw) == len(cw):
|
| 1919 |
+
# Check char-by-char differences
|
| 1920 |
+
for ic, cc in zip(iw, cw):
|
| 1921 |
+
if ic != cc and RulesBasedCorrector.is_keyboard_neighbor(ic, cc):
|
| 1922 |
+
keyboard_bonus *= 1.05 # 5% bonus per keyboard-adjacent fix
|
| 1923 |
+
|
| 1924 |
+
# HIGH CONFIDENCE GATING
|
| 1925 |
+
# If model is extremely confident (high fluency) and words are valid, relax validation
|
| 1926 |
+
# This allows correcting severe corruptions that fail strict edit distance
|
| 1927 |
+
if fluency_score > 0.85 and cand_oov_count == 0:
|
| 1928 |
+
if not is_valid and reason in ["too_short", "low_character_similarity", "word_count_mismatch"]:
|
| 1929 |
+
# Check if it makes sense length-wise (don't allow completely empty or massive hallucinations)
|
| 1930 |
+
if len(cand) >= len(original) * 0.4:
|
| 1931 |
+
is_valid = True
|
| 1932 |
+
reason = "high_confidence_override"
|
| 1933 |
+
vocab_boost *= 1.2 # Bonus for high confidence
|
| 1934 |
+
validity_factor = 1.0 # Reset validity factor
|
| 1935 |
+
|
| 1936 |
+
# Final Score = (Fluency^0.3) * (Similarity^3.0) * Validity * VocabBoost * KeyboardBonus * BeamBoost
|
| 1937 |
+
fluency_exp = 0.3
|
| 1938 |
+
similarity_exp = 3.0
|
| 1939 |
+
|
| 1940 |
+
# Beam 0 Boost — model's top beam gets 15% priority
|
| 1941 |
+
beam_boost = 1.0
|
| 1942 |
+
if raw_model_output and cand == raw_model_output:
|
| 1943 |
+
beam_boost = 1.15
|
| 1944 |
+
|
| 1945 |
+
final_score = (fluency_score ** fluency_exp) * (similarity ** similarity_exp) * validity_factor * vocab_boost * keyboard_bonus * beam_boost
|
| 1946 |
+
|
| 1947 |
+
candidate_scores.append({
|
| 1948 |
+
'text': cand,
|
| 1949 |
+
'is_valid': is_valid,
|
| 1950 |
+
'reason': reason,
|
| 1951 |
+
'fluency': fluency_score,
|
| 1952 |
+
'similarity': similarity,
|
| 1953 |
+
'vocab_boost': vocab_boost,
|
| 1954 |
+
'input_oov': input_oov_count,
|
| 1955 |
+
'cand_oov': cand_oov_count,
|
| 1956 |
+
'final_score': final_score
|
| 1957 |
+
})
|
| 1958 |
+
|
| 1959 |
+
if final_score > best_score:
|
| 1960 |
+
best_score = final_score
|
| 1961 |
+
best_candidate = cand
|
| 1962 |
+
|
| 1963 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 1964 |
+
# --- Output Quality Scoring (Minimum Score Threshold) ---
|
| 1965 |
+
# If ALL candidates scored poorly, the correction is unreliable → keep input
|
| 1966 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 1967 |
+
if best_candidate != preprocessed_text:
|
| 1968 |
+
# Check: did the best candidate actually get a decent score?
|
| 1969 |
+
# The preprocessed input (candidate 0) is always in the pool.
|
| 1970 |
+
# If the best candidate barely beats preprocessed_text, it might not be trustworthy.
|
| 1971 |
+
preprocessed_score = 0.0
|
| 1972 |
+
for cs in candidate_scores:
|
| 1973 |
+
if cs['text'] == preprocessed_text:
|
| 1974 |
+
preprocessed_score = cs['final_score']
|
| 1975 |
+
break
|
| 1976 |
+
|
| 1977 |
+
# If best score is less than 1.05x the preprocessed score AND
|
| 1978 |
+
# the best candidate introduced OOV words → fall back to preprocessed
|
| 1979 |
+
if preprocessed_score > 0 and best_score < preprocessed_score * 1.05:
|
| 1980 |
+
best_oov = self.vocab_manager.count_oov_words(best_candidate)
|
| 1981 |
+
prep_oov = self.vocab_manager.count_oov_words(preprocessed_text)
|
| 1982 |
+
if best_oov > prep_oov:
|
| 1983 |
+
best_candidate = preprocessed_text
|
| 1984 |
+
best_score = preprocessed_score
|
| 1985 |
+
|
| 1986 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 1987 |
+
# --- Contextual Validation Layer ---
|
| 1988 |
+
# Compare fluency of input vs best candidate
|
| 1989 |
+
# If correction made text LESS fluent → reject the correction
|
| 1990 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 1991 |
+
if best_candidate != preprocessed_text and self.use_contextual and self.contextual:
|
| 1992 |
+
try:
|
| 1993 |
+
input_fluency = self.contextual.calculate_sentence_score(preprocessed_text)
|
| 1994 |
+
best_fluency = 0.0
|
| 1995 |
+
for cs in candidate_scores:
|
| 1996 |
+
if cs['text'] == best_candidate:
|
| 1997 |
+
best_fluency = cs['fluency']
|
| 1998 |
+
break
|
| 1999 |
+
|
| 2000 |
+
# If input is significantly more fluent than best candidate
|
| 2001 |
+
# AND both have similar OOV counts → prefer input
|
| 2002 |
+
if input_fluency > 0 and best_fluency > 0:
|
| 2003 |
+
if input_fluency > best_fluency * 1.5: # Input 50% more fluent
|
| 2004 |
+
input_oov = self.vocab_manager.count_oov_words(preprocessed_text)
|
| 2005 |
+
best_oov = self.vocab_manager.count_oov_words(best_candidate)
|
| 2006 |
+
if input_oov <= best_oov:
|
| 2007 |
+
# Input is more fluent AND has fewer/equal OOV → keep input
|
| 2008 |
+
best_candidate = preprocessed_text
|
| 2009 |
+
except Exception:
|
| 2010 |
+
pass # Contextual validation is optional
|
| 2011 |
+
|
| 2012 |
+
# 5. Postprocess Winner
|
| 2013 |
+
result = self.postprocess(best_candidate, original)
|
| 2014 |
+
|
| 2015 |
+
# 5.5 IV-Safe Postprocessing Check
|
| 2016 |
+
# If postprocessing changed an IV word to OOV, revert that word
|
| 2017 |
+
if result != best_candidate:
|
| 2018 |
+
result_words = result.split()
|
| 2019 |
+
best_words = best_candidate.split()
|
| 2020 |
+
if len(result_words) == len(best_words):
|
| 2021 |
+
fixed_words = []
|
| 2022 |
+
input_words_pp = preprocessed_text.split()
|
| 2023 |
+
for idx_fw, (rw, bw) in enumerate(zip(result_words, best_words)):
|
| 2024 |
+
if rw != bw:
|
| 2025 |
+
# Postprocessor changed this word
|
| 2026 |
+
bw_iv = self.vocab_manager.is_iv(bw)
|
| 2027 |
+
rw_iv = self.vocab_manager.is_iv(rw)
|
| 2028 |
+
if bw_iv and not rw_iv:
|
| 2029 |
+
# IV → OOV: revert to pre-postprocess version
|
| 2030 |
+
fixed_words.append(bw)
|
| 2031 |
+
elif bw_iv and rw_iv:
|
| 2032 |
+
# Postprocess Distance Guard
|
| 2033 |
+
# DISABLED: Caused word-level regression. When both are IV,
|
| 2034 |
+
# the postprocessor's choice (rw) is usually better because
|
| 2035 |
+
# it applies Arabic-specific rules (hamza, ta marbuta).
|
| 2036 |
+
fixed_words.append(rw)
|
| 2037 |
+
else:
|
| 2038 |
+
fixed_words.append(rw)
|
| 2039 |
+
else:
|
| 2040 |
+
fixed_words.append(rw)
|
| 2041 |
+
result = ' '.join(fixed_words)
|
| 2042 |
+
|
| 2043 |
+
# 6. Contextual fine-tuning (BERT Masked Refinement)
|
| 2044 |
+
# IV-Safe mode - pass vocab_manager to protect IV words
|
| 2045 |
+
# BERT Kill Switch - also pass raw_model_output to protect model-confident words
|
| 2046 |
+
if self.use_contextual and self.contextual:
|
| 2047 |
+
if len(result) > 3:
|
| 2048 |
+
result = self.contextual.refine_sentence_with_mask(
|
| 2049 |
+
result, vocab_manager=self.vocab_manager,
|
| 2050 |
+
raw_model_output=raw_model_output
|
| 2051 |
+
)
|
| 2052 |
+
|
| 2053 |
+
# 7. Safe Split/Merge Post-processing
|
| 2054 |
+
# Only apply merge_fragments (safe: only merges when result is IV)
|
| 2055 |
+
result = self.split_merge.merge_fragments(result)
|
| 2056 |
+
|
| 2057 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 2058 |
+
# VALIDATION & QUALITY CHECKS
|
| 2059 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 2060 |
+
|
| 2061 |
+
# 8. Output Stability Test (Solution 30)
|
| 2062 |
+
# If correcting the output again changes it → unstable correction → reject
|
| 2063 |
+
# Stable corrections are idempotent: correct(correct(x)) == correct(x)
|
| 2064 |
+
if result != preprocessed_text and raw_model_output:
|
| 2065 |
+
try:
|
| 2066 |
+
# Quick stability check: run the result through preprocessing only
|
| 2067 |
+
# (full model inference would be too slow)
|
| 2068 |
+
re_preprocessed = self.preprocess(result)
|
| 2069 |
+
|
| 2070 |
+
# If re-preprocessing changes the result significantly, it was unstable
|
| 2071 |
+
stability_dist = VocabularyManager.damerau_levenshtein_distance(result, re_preprocessed)
|
| 2072 |
+
result_len = max(len(result), 1)
|
| 2073 |
+
|
| 2074 |
+
if stability_dist > 0:
|
| 2075 |
+
# Result is not stable under re-preprocessing
|
| 2076 |
+
stability_ratio = stability_dist / result_len
|
| 2077 |
+
|
| 2078 |
+
if stability_ratio > 0.15: # More than 15% changed → very unstable
|
| 2079 |
+
# Fall back to raw model output if it's more stable
|
| 2080 |
+
raw_re = self.preprocess(raw_model_output)
|
| 2081 |
+
raw_stability = VocabularyManager.damerau_levenshtein_distance(
|
| 2082 |
+
raw_model_output, raw_re
|
| 2083 |
+
) / max(len(raw_model_output), 1)
|
| 2084 |
+
|
| 2085 |
+
if raw_stability < stability_ratio:
|
| 2086 |
+
# Raw is more stable → use it
|
| 2087 |
+
raw_oov = self.vocab_manager.count_oov_words(raw_model_output)
|
| 2088 |
+
our_oov = self.vocab_manager.count_oov_words(result)
|
| 2089 |
+
if raw_oov <= our_oov:
|
| 2090 |
+
result = raw_model_output
|
| 2091 |
+
except Exception:
|
| 2092 |
+
pass # Stability check is optional, don't break pipeline
|
| 2093 |
+
|
| 2094 |
+
# 9. Bidirectional Word-Level Validation (Solution 24)
|
| 2095 |
+
# Compare our result word-by-word with raw model output
|
| 2096 |
+
# If we corrupted a word that the model got right, revert that word
|
| 2097 |
+
if raw_model_output and result != raw_model_output:
|
| 2098 |
+
result_words = result.split()
|
| 2099 |
+
raw_words = raw_model_output.split()
|
| 2100 |
+
|
| 2101 |
+
if len(result_words) == len(raw_words):
|
| 2102 |
+
corrected_words = []
|
| 2103 |
+
changed = False
|
| 2104 |
+
|
| 2105 |
+
for rw, raw_w in zip(result_words, raw_words):
|
| 2106 |
+
if rw != raw_w:
|
| 2107 |
+
rw_iv = self.vocab_manager.is_iv(rw)
|
| 2108 |
+
raw_iv = self.vocab_manager.is_iv(raw_w)
|
| 2109 |
+
|
| 2110 |
+
# Case 1: Our word is OOV but raw word is IV → take raw
|
| 2111 |
+
if not rw_iv and raw_iv:
|
| 2112 |
+
corrected_words.append(raw_w)
|
| 2113 |
+
changed = True
|
| 2114 |
+
# Case 2: Both IV but our word is further from input
|
| 2115 |
+
elif rw_iv and raw_iv:
|
| 2116 |
+
# Find corresponding input word
|
| 2117 |
+
input_words = preprocessed_text.split()
|
| 2118 |
+
idx = len(corrected_words)
|
| 2119 |
+
if idx < len(input_words):
|
| 2120 |
+
input_w = input_words[idx]
|
| 2121 |
+
rw_dist = Levenshtein.distance(input_w, rw)
|
| 2122 |
+
raw_dist = Levenshtein.distance(input_w, raw_w)
|
| 2123 |
+
# If raw is closer to input AND both are IV → prefer raw
|
| 2124 |
+
# (our pipeline likely introduced unnecessary change)
|
| 2125 |
+
if raw_dist < rw_dist:
|
| 2126 |
+
corrected_words.append(raw_w)
|
| 2127 |
+
changed = True
|
| 2128 |
+
else:
|
| 2129 |
+
corrected_words.append(rw)
|
| 2130 |
+
else:
|
| 2131 |
+
corrected_words.append(rw)
|
| 2132 |
+
else:
|
| 2133 |
+
corrected_words.append(rw)
|
| 2134 |
+
else:
|
| 2135 |
+
corrected_words.append(rw)
|
| 2136 |
+
|
| 2137 |
+
if changed:
|
| 2138 |
+
new_result = ' '.join(corrected_words)
|
| 2139 |
+
# Only accept if the new result doesn't increase OOV
|
| 2140 |
+
new_oov = self.vocab_manager.count_oov_words(new_result)
|
| 2141 |
+
old_oov = self.vocab_manager.count_oov_words(result)
|
| 2142 |
+
if new_oov <= old_oov:
|
| 2143 |
+
result = new_result
|
| 2144 |
+
|
| 2145 |
+
# 10. SAFETY NET: Compare with raw model output (Conservative)
|
| 2146 |
+
# Only switch to raw if raw is CLEARLY better
|
| 2147 |
+
if raw_model_output and raw_model_output != result:
|
| 2148 |
+
raw_oov = self.vocab_manager.count_oov_words(raw_model_output)
|
| 2149 |
+
our_oov = self.vocab_manager.count_oov_words(result)
|
| 2150 |
+
|
| 2151 |
+
# Case A: Raw all-IV, ours has OOV
|
| 2152 |
+
if raw_oov == 0 and our_oov > 0:
|
| 2153 |
+
is_valid, reason = self.validator.validate(original, raw_model_output, "mixed")
|
| 2154 |
+
if is_valid or reason == "space_leniency_accept":
|
| 2155 |
+
result = raw_model_output
|
| 2156 |
+
|
| 2157 |
+
# Case B: Both all-IV but raw is more similar to input
|
| 2158 |
+
# Catches BERT/postprocess damage (word substitutions up to 5 char distance)
|
| 2159 |
+
elif raw_oov == 0 and our_oov == 0:
|
| 2160 |
+
raw_dist = VocabularyManager.damerau_levenshtein_distance(original, raw_model_output)
|
| 2161 |
+
our_dist = VocabularyManager.damerau_levenshtein_distance(original, result)
|
| 2162 |
+
result_vs_raw_dist = VocabularyManager.damerau_levenshtein_distance(result, raw_model_output)
|
| 2163 |
+
# Threshold at 3 chars — covers single char edits and small substitutions
|
| 2164 |
+
# (widening to 5 caused regression by reverting valid hybrid corrections)
|
| 2165 |
+
if raw_dist < our_dist and result_vs_raw_dist <= 3:
|
| 2166 |
+
raw_valid, _ = self.validator.validate(original, raw_model_output, "mixed")
|
| 2167 |
+
if raw_valid:
|
| 2168 |
+
result = raw_model_output
|
| 2169 |
+
|
| 2170 |
+
# Case C: Word count differs — raw might have correct splitting
|
| 2171 |
+
# Catches: 'فيلق → في فيلق' (pipeline added word)
|
| 2172 |
+
# or 'بلاكبيرن روفرز → بلاكبيرن روفر' (pipeline lost word ending)
|
| 2173 |
+
elif raw_oov == 0:
|
| 2174 |
+
raw_wc = len(raw_model_output.split())
|
| 2175 |
+
our_wc = len(result.split())
|
| 2176 |
+
if raw_wc != our_wc:
|
| 2177 |
+
raw_dist = VocabularyManager.damerau_levenshtein_distance(original, raw_model_output)
|
| 2178 |
+
our_dist = VocabularyManager.damerau_levenshtein_distance(original, result)
|
| 2179 |
+
if raw_dist < our_dist:
|
| 2180 |
+
raw_valid, _ = self.validator.validate(original, raw_model_output, "mixed")
|
| 2181 |
+
if raw_valid:
|
| 2182 |
+
result = raw_model_output
|
| 2183 |
+
|
| 2184 |
+
return result
|
| 2185 |
+
|
| 2186 |
+
# ═══════════════════════════════════════════════════════════════════════════════
|
| 2187 |
+
# PUBLIC API
|
| 2188 |
+
# ═══════════════════════════════════════════════════════════════════════════════
|
| 2189 |
+
|
| 2190 |
+
# Exported for use by benchmark.py and external consumers
|
| 2191 |
+
spell_checker = None # Will be initialized on first import with __main__ or by benchmark
|
| 2192 |
+
|
| 2193 |
+
|
| 2194 |
+
def initialize(use_contextual=True):
|
| 2195 |
+
"""Initialize the spell checker. Call once before using."""
|
| 2196 |
+
global spell_checker
|
| 2197 |
+
spell_checker = ArabicSpellChecker(model, tokenizer, device, use_contextual=use_contextual)
|
| 2198 |
+
logger.info("Spell checker initialized")
|
| 2199 |
+
return spell_checker
|
| 2200 |
+
|
| 2201 |
+
|
| 2202 |
+
if __name__ == "__main__":
|
| 2203 |
+
sc = initialize(use_contextual=True)
|
| 2204 |
+
|
| 2205 |
+
# Quick demo
|
| 2206 |
+
test_cases = [
|
| 2207 |
+
"السلام عليكممم",
|
| 2208 |
+
"فيالمدرسه",
|
| 2209 |
+
"الطقص جميل اليومم",
|
| 2210 |
+
]
|
| 2211 |
+
|
| 2212 |
+
print("\n" + "=" * 60)
|
| 2213 |
+
print("AraSpell Demo")
|
| 2214 |
+
print("=" * 60)
|
| 2215 |
+
|
| 2216 |
+
for text in test_cases:
|
| 2217 |
+
corrected = sc.correct(text)
|
| 2218 |
+
print(f"\n Input: {text}")
|
| 2219 |
+
print(f" Corrected: {corrected}")
|
| 2220 |
+
|
| 2221 |
+
print("\n" + "=" * 60)
|
| 2222 |
+
print("For full benchmark, run: python benchmark.py")
|
| 2223 |
+
print("=" * 60)
|
| 2224 |
+
|
archive/legacy_scripts/Grammer_Rules.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
from camel_tools.tokenizers.word import simple_word_tokenize
|
| 3 |
+
from camel_tools.disambig.mle import MLEDisambiguator
|
| 4 |
+
|
| 5 |
+
class ArabicGrammarGuard:
|
| 6 |
+
def __init__(self):
|
| 7 |
+
|
| 8 |
+
self.mle = MLEDisambiguator.pretrained()
|
| 9 |
+
|
| 10 |
+
self.number_words = ["واحد", "اثنان", "اثنين", "ثلاث", "أربع", "خمس", "ست", "سبع", "ثمان", "تسع", "عشر",
|
| 11 |
+
"عشرون", "عشرين", "ثلاثون", "ثلاثين", "أربعون", "أربعين", "خمسون", "خمسين",
|
| 12 |
+
"ستون", "ستين", "سبعون", "سبعين", "ثمانون", "ثمانين", "تسعون", "تسعين", "مائة", "ألف"]
|
| 13 |
+
|
| 14 |
+
self.asmaa_khamsa_roots = ['اب', 'اخ', 'حم', 'فو', 'ذو']
|
| 15 |
+
|
| 16 |
+
def preserve_numbers(self, original_text, generated_text):
|
| 17 |
+
orig_digits = re.findall(r'\d+', original_text)
|
| 18 |
+
gen_digits = re.findall(r'\d+', generated_text)
|
| 19 |
+
if orig_digits and gen_digits and orig_digits != gen_digits:
|
| 20 |
+
return original_text
|
| 21 |
+
|
| 22 |
+
orig_words = [w for w in original_text.split() if any(num in w for num in self.number_words)]
|
| 23 |
+
gen_words = [w for w in generated_text.split() if any(num in w for num in self.number_words)]
|
| 24 |
+
if len(orig_words) > 0 and len(gen_words) > 0:
|
| 25 |
+
if not any(orig[:3] in gen for orig in orig_words for gen in gen_words):
|
| 26 |
+
return original_text
|
| 27 |
+
return generated_text
|
| 28 |
+
|
| 29 |
+
def fix_number_and_gender_agreement(self, text):
|
| 30 |
+
tokens = simple_word_tokenize(text)
|
| 31 |
+
disambig_tokens = self.mle.disambiguate(tokens)
|
| 32 |
+
corrected_tokens = list(tokens)
|
| 33 |
+
|
| 34 |
+
for i in range(len(disambig_tokens) - 1):
|
| 35 |
+
w1_info = disambig_tokens[i].analyses[0] if disambig_tokens[i].analyses else None
|
| 36 |
+
w2_info = disambig_tokens[i+1].analyses[0] if disambig_tokens[i+1].analyses else None
|
| 37 |
+
if not w1_info or not w2_info: continue
|
| 38 |
+
|
| 39 |
+
w1_pos = w1_info.analysis.get('pos', 'unknown')
|
| 40 |
+
w2_pos = w2_info.analysis.get('pos', 'unknown')
|
| 41 |
+
w1_word = corrected_tokens[i]
|
| 42 |
+
w2_word = corrected_tokens[i+1]
|
| 43 |
+
|
| 44 |
+
if w1_pos == 'verb' and w2_pos == 'noun':
|
| 45 |
+
if (w1_word.endswith('ون') or w1_word.endswith('وا')) and (w2_word.endswith('ون') or w2_word.endswith('ين')):
|
| 46 |
+
if w1_word.endswith('ون'): corrected_tokens[i] = w1_word[:-2]
|
| 47 |
+
elif w1_word.endswith('وا'): corrected_tokens[i] = w1_word[:-2]
|
| 48 |
+
|
| 49 |
+
elif w1_pos == 'noun' and w2_pos == 'verb':
|
| 50 |
+
if w1_word.endswith('ون') and not (w2_word.endswith('ون') or w2_word.endswith('وا') or w2_word.endswith('ين')):
|
| 51 |
+
if w2_info.analysis.get('num') == 's':
|
| 52 |
+
corrected_tokens[i+1] = w2_word + 'ون'
|
| 53 |
+
|
| 54 |
+
# ⚠️ التعديل الجذري هنا: المطابقة للصفات (adj) فقط، ومنع الكلمات التي تبدأ بـ "ب" أو تنتهي بألف التنوين
|
| 55 |
+
elif w1_pos == 'noun' and w2_pos == 'adj':
|
| 56 |
+
if w1_word.endswith('ون') and not w2_word.endswith('ون'):
|
| 57 |
+
if w2_info.analysis.get('num') == 's' and w2_info.analysis.get('gen') == 'm':
|
| 58 |
+
if len(w2_word) > 2 and not w2_word.endswith('ا') and not w2_word.startswith('ب'):
|
| 59 |
+
corrected_tokens[i+1] = w2_word + 'ون'
|
| 60 |
+
|
| 61 |
+
return " ".join(corrected_tokens)
|
| 62 |
+
|
| 63 |
+
def smart_asmaa_khamsa_fix(self, text):
|
| 64 |
+
tokens = simple_word_tokenize(text)
|
| 65 |
+
disambig_tokens = self.mle.disambiguate(tokens)
|
| 66 |
+
corrected_tokens = []
|
| 67 |
+
verb_seen = False
|
| 68 |
+
|
| 69 |
+
for i, token_info in enumerate(disambig_tokens):
|
| 70 |
+
word = tokens[i]
|
| 71 |
+
|
| 72 |
+
pos_tag = token_info.analyses[0].analysis.get('pos', 'unknown') if token_info.analyses else 'unknown'
|
| 73 |
+
|
| 74 |
+
if pos_tag == 'verb':
|
| 75 |
+
verb_seen = True
|
| 76 |
+
corrected_tokens.append(word)
|
| 77 |
+
continue
|
| 78 |
+
|
| 79 |
+
is_asmaa = any(word.startswith(root) or word.startswith('أ' + root[1:]) for root in self.asmaa_khamsa_roots if len(root)>1)
|
| 80 |
+
|
| 81 |
+
if is_asmaa and len(word) >= 3:
|
| 82 |
+
if verb_seen:
|
| 83 |
+
word = word.replace('ا', 'و').replace('ي', 'و')
|
| 84 |
+
verb_seen = False
|
| 85 |
+
|
| 86 |
+
corrected_tokens.append(word)
|
| 87 |
+
|
| 88 |
+
return " ".join(corrected_tokens)
|
| 89 |
+
|
| 90 |
+
def fix_verbs_nasb_and_jazm(self, text):
|
| 91 |
+
tokens = simple_word_tokenize(text)
|
| 92 |
+
disambig_tokens = self.mle.disambiguate(tokens)
|
| 93 |
+
|
| 94 |
+
nasb_particles = ['أن', 'لن', 'كي', 'لكي', 'حتى', 'إذن']
|
| 95 |
+
jazm_particles = ['لم', 'لما', 'لا']
|
| 96 |
+
|
| 97 |
+
corrected_tokens = []
|
| 98 |
+
|
| 99 |
+
for i, token_info in enumerate(disambig_tokens):
|
| 100 |
+
word = tokens[i]
|
| 101 |
+
|
| 102 |
+
pos_tag = token_info.analyses[0].analysis.get('pos', 'unknown') if token_info.analyses else 'unknown'
|
| 103 |
+
|
| 104 |
+
is_nasb_context = False
|
| 105 |
+
is_jazm_context = False
|
| 106 |
+
|
| 107 |
+
if i > 0:
|
| 108 |
+
prev_word = tokens[i-1]
|
| 109 |
+
if prev_word in nasb_particles or word.startswith('ل'):
|
| 110 |
+
is_nasb_context = True
|
| 111 |
+
if prev_word in jazm_particles or word.startswith('ل') or word.startswith('ول'):
|
| 112 |
+
is_jazm_context = True
|
| 113 |
+
|
| 114 |
+
if pos_tag == 'verb' and (is_nasb_context or is_jazm_context):
|
| 115 |
+
if word.endswith('ون'):
|
| 116 |
+
word = word[:-2] + 'وا'
|
| 117 |
+
elif word.endswith('ان'):
|
| 118 |
+
word = word[:-2] + 'ا'
|
| 119 |
+
elif word.endswith('ين'):
|
| 120 |
+
word = word[:-2] + 'ي'
|
| 121 |
+
elif is_jazm_context:
|
| 122 |
+
if word.endswith('و') and len(word) > 3:
|
| 123 |
+
word = word[:-1] + 'ُ'
|
| 124 |
+
elif (word.endswith('i') or word.endswith('ي')) and len(word) > 3:
|
| 125 |
+
if word.endswith('ي'): word = word[:-1] + 'ِ'
|
| 126 |
+
elif (word.endswith('ى') or word.endswith('ا')) and len(word) > 3:
|
| 127 |
+
word = word[:-1] + 'َ'
|
| 128 |
+
|
| 129 |
+
corrected_tokens.append(word)
|
| 130 |
+
return " ".join(corrected_tokens)
|
| 131 |
+
|
| 132 |
+
def fix_gender_agreement(self, text):
|
| 133 |
+
text = re.sub(r'\bهذان\s+(ال[أ-ي]+تان)\b', r'هاتان \1', text)
|
| 134 |
+
text = re.sub(r'\bهاتان\s+(ال[أ-ي]+[^ت]ان)\b', r'هذان \1', text)
|
| 135 |
+
text = re.sub(r'\bهذهن\b', 'هاتان', text)
|
| 136 |
+
|
| 137 |
+
text = re.sub(r'\bأحد عشر\s+([أ-ي]+ة)\b', r'إحدى عشرة \1', text)
|
| 138 |
+
text = re.sub(r'\bأحد عشرة\s+([أ-ي]+ة)\b', r'إحدى عشرة \1', text)
|
| 139 |
+
|
| 140 |
+
text = re.sub(r'\bإحدى عشرة\s+([أ-ي]+ا|رجل[اأ]|طالب[اأ]|مهندس[اأ])\b', r'أحد عشر \1', text)
|
| 141 |
+
text = re.sub(r'\bإحدى عشر\s+([أ-ي]+ا|رجل[اأ]|طالب[اأ]|مهندس[اأ])\b', r'أحد عشر \1', text)
|
| 142 |
+
return text
|
| 143 |
+
|
| 144 |
+
def fix_prepositions_advanced(self, text):
|
| 145 |
+
# ⚠️ السماح بحروف العطف (و، ف) قبل حرف الجر
|
| 146 |
+
# (في المهندسون) -> (في المهندسين)
|
| 147 |
+
text = re.sub(r'\b([وف]?(?:في|من|إلى|على|عن|حتى))\s+([أ-ي]{2,})(ون|ان)\b', r'\1 \2ين', text)
|
| 148 |
+
|
| 149 |
+
# (وبالمبرمجون) -> (وبالمبرمجين)
|
| 150 |
+
text = re.sub(r'\b([وف]?[بلكف])ال([أ-ي]{2,})(ون|ان)\b', r'\1ال\2ين', text)
|
| 151 |
+
|
| 152 |
+
# (ولمهندسون) -> (ولمهندسين)
|
| 153 |
+
text = re.sub(r'\b([وف]?ل)([أ-ي]{2,})(ون|ان)\b', r'\1\2ين', text)
|
| 154 |
+
return text
|
| 155 |
+
|
| 156 |
+
def regex_rules_fallback(self, text):
|
| 157 |
+
# إن وأخواتها (كما هي)
|
| 158 |
+
text = re.sub(r'\b(إن|أن|كأن|لكن|لعل|ليت)\s+(أبوك|أخوك|ذو|فوك)\b',
|
| 159 |
+
lambda m: f"{m.group(1)} {m.group(2).replace('و', 'ا')}", text)
|
| 160 |
+
|
| 161 |
+
# ⚠️ حروف الجر المنفصلة بمسافة (في أخوك -> في أخيك)
|
| 162 |
+
text = re.sub(r'\b([وف]?(?:في|من|إلى|على|عن))\s+(أبوك|أباك|أخوك|أخاك|ذو|ذا)\b',
|
| 163 |
+
lambda m: f"{m.group(1)} {m.group(2).replace('و', 'ي').replace('ا', 'ي')}", text)
|
| 164 |
+
|
| 165 |
+
# ⚠️ حروف الجر المتصلة بدون مسافة (بأخوك، لأبوك -> بأخيك، لأبيك)
|
| 166 |
+
text = re.sub(r'\b([وف]?[بل])(أبوك|أباك|أخوك|أخاك|ذو|ذا)\b',
|
| 167 |
+
lambda m: f"{m.group(1)}{m.group(2).replace('و', 'ي').replace('ا', 'ي')}", text)
|
| 168 |
+
return text
|
| 169 |
+
|
| 170 |
+
def process(self, original_text, generated_text):
|
| 171 |
+
text = self.preserve_numbers(original_text, generated_text)
|
| 172 |
+
text = self.fix_number_and_gender_agreement(text)
|
| 173 |
+
text = self.smart_asmaa_khamsa_fix(text)
|
| 174 |
+
text = self.fix_verbs_nasb_and_jazm(text)
|
| 175 |
+
text = self.fix_gender_agreement(text)
|
| 176 |
+
text = self.fix_prepositions_advanced(text)
|
| 177 |
+
text = self.regex_rules_fallback(text)
|
| 178 |
+
text = re.sub(r'\s+', ' ', text).strip()
|
| 179 |
+
return text
|
archive/legacy_scripts/PuncAra.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""Untitled18.ipynb
|
| 3 |
+
|
| 4 |
+
Automatically generated by Colab.
|
| 5 |
+
|
| 6 |
+
Original file is located at
|
| 7 |
+
https://colab.research.google.com/drive/1ebBGzEo4wbwwvReea_n0PRHdfYescKcs
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import os
|
| 11 |
+
import torch
|
| 12 |
+
from transformers import EncoderDecoderModel, AutoTokenizer
|
| 13 |
+
import re
|
| 14 |
+
|
| 15 |
+
# تعريف الثوابت
|
| 16 |
+
HF_REPO_ID = "bayan10/PuncAra-v1"
|
| 17 |
+
|
| 18 |
+
# متغيرات عامة
|
| 19 |
+
device = None
|
| 20 |
+
test_model = None
|
| 21 |
+
test_tokenizer = None
|
| 22 |
+
|
| 23 |
+
def initialize_model(repo_id=HF_REPO_ID):
|
| 24 |
+
"""
|
| 25 |
+
تهيئة وإعداد كرت الشاشة وتحميل النموذج والـ Tokenizer من Hugging Face Hub.
|
| 26 |
+
يتم استدعاء هذه الدالة مرة واحدة فقط في بداية تشغيل المشروع.
|
| 27 |
+
"""
|
| 28 |
+
global device, test_model, test_tokenizer
|
| 29 |
+
print(f"Loading test model directly from Hugging Face Hub: {repo_id}")
|
| 30 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 31 |
+
print(f"Loading test model to: {device}")
|
| 32 |
+
|
| 33 |
+
if device.type == "cuda" and not torch.cuda.is_available():
|
| 34 |
+
print("Warning: CUDA device requested, but torch.cuda.is_available() is False. Model will be loaded to CPU.")
|
| 35 |
+
device = torch.device("cpu")
|
| 36 |
+
|
| 37 |
+
test_model = EncoderDecoderModel.from_pretrained(repo_id).to(device)
|
| 38 |
+
test_tokenizer = AutoTokenizer.from_pretrained(repo_id)
|
| 39 |
+
|
| 40 |
+
# إعداد الـ Special tokens للـ Decoder والـ Encoder
|
| 41 |
+
test_model.config.decoder_start_token_id = test_tokenizer.cls_token_id
|
| 42 |
+
test_model.config.bos_token_id = test_tokenizer.cls_token_id
|
| 43 |
+
test_model.config.eos_token_id = test_tokenizer.sep_token_id
|
| 44 |
+
test_model.config.pad_token_id = test_tokenizer.pad_token_id
|
| 45 |
+
print("Model and Tokenizer loaded successfully!")
|
| 46 |
+
|
| 47 |
+
def predict_chunk(text_chunk):
|
| 48 |
+
"""توليد التوقعات لعلامات الترقيم لقطعة نصية صغيرة لا تتعدى الـ 128 Token."""
|
| 49 |
+
global device, test_model, test_tokenizer
|
| 50 |
+
if test_model is None or test_tokenizer is None:
|
| 51 |
+
raise RuntimeError("الموديل لم يتم تهيئته بعد. يرجى استدعاء initialize_model() أولاً.")
|
| 52 |
+
|
| 53 |
+
# تطبيق الـ Preprocessing لتنظيف التشكيل قبل دخول النص للموديل
|
| 54 |
+
text_chunk = arabic_preprocessing(text_chunk)
|
| 55 |
+
|
| 56 |
+
inputs = test_tokenizer(text_chunk, return_tensors="pt", padding=True, truncation=True, max_length=128).to(device)
|
| 57 |
+
|
| 58 |
+
outputs = test_model.generate(
|
| 59 |
+
inputs.input_ids,
|
| 60 |
+
attention_mask=inputs.attention_mask,
|
| 61 |
+
decoder_start_token_id=test_tokenizer.cls_token_id,
|
| 62 |
+
bos_token_id=test_tokenizer.cls_token_id,
|
| 63 |
+
eos_token_id=test_tokenizer.sep_token_id,
|
| 64 |
+
pad_token_id=test_tokenizer.pad_token_id,
|
| 65 |
+
max_length=128,
|
| 66 |
+
num_beams=3,
|
| 67 |
+
repetition_penalty=1.2,
|
| 68 |
+
length_penalty=1.0,
|
| 69 |
+
early_stopping=True,
|
| 70 |
+
do_sample=False
|
| 71 |
+
)
|
| 72 |
+
return test_tokenizer.decode(outputs[0], skip_special_tokens=True)
|
| 73 |
+
|
| 74 |
+
def arabic_preprocessing(text):
|
| 75 |
+
"""حذف الحركات التشكيلية لتوحيد المدخلات وتسهيل عمل الموديل."""
|
| 76 |
+
arabic_diacritics = re.compile(r'[\u064B-\u0652]')
|
| 77 |
+
return re.sub(arabic_diacritics, '', text).strip()
|
| 78 |
+
|
| 79 |
+
def arabic_postprocessing(text):
|
| 80 |
+
"""
|
| 81 |
+
التنظيف والتحسين المطبعي وعلاج مشاكل دمج النصوص وعلامات الترقيم الزائدة.
|
| 82 |
+
"""
|
| 83 |
+
if not text:
|
| 84 |
+
return text
|
| 85 |
+
|
| 86 |
+
# 1. حماية الأرقام والكسور والتوقيت من التحويل الخاطئ
|
| 87 |
+
text = re.sub(r'(?<=\d),(?=\d)', '٪TEMP_COMMA٪', text)
|
| 88 |
+
text = re.sub(r'(?<=\d):(?=\d)', '٪TEMP_COLON٪', text)
|
| 89 |
+
|
| 90 |
+
# 2. التوحيد والتعريب المطبعي للعلامات
|
| 91 |
+
text = text.replace(',', '،').replace(';', '؛').replace('?', '؟')
|
| 92 |
+
|
| 93 |
+
# 3. ضبط المسافات الداخلية للأقواس وعلامات الاقتباس العربي
|
| 94 |
+
text = re.sub(r'\(\s+', '(', text)
|
| 95 |
+
text = re.sub(r'\s+\)', ')', text)
|
| 96 |
+
text = re.sub(r'\[\s+', '[', text)
|
| 97 |
+
text = re.sub(r'\s+\]', ']', text)
|
| 98 |
+
text = re.sub(r'«\s+', '«', text)
|
| 99 |
+
text = re.sub(r'\s+»', '»', text)
|
| 100 |
+
|
| 101 |
+
# 4. منع تكرار العلامات الانفعالية عدا النقاط الثلاثية للحذف
|
| 102 |
+
text = re.sub(r'([،؛:!؟])\1+', r'\1', text)
|
| 103 |
+
text = re.sub(r'\.{4,}', '...', text)
|
| 104 |
+
|
| 105 |
+
# 5. معالجة التناقضات المباشرة الناتجة عن تجميع الـ Chunks
|
| 106 |
+
text = re.sub(r'[،؛:]+([.!؟])', r'\1', text)
|
| 107 |
+
text = re.sub(r'،؛|؛،', '؛', text)
|
| 108 |
+
text = re.sub(r'([!؟])\.', r'\1', text)
|
| 109 |
+
|
| 110 |
+
# 6. مسح علامات الترقيم العشوائية إذا ظهرت أول النص
|
| 111 |
+
text = re.sub(r'^[،؛:!؟. \t]+', '', text)
|
| 112 |
+
|
| 113 |
+
# 7. ضمان مسافة فارغة واحدة بعد علامة الترقيم إذا تبعها كلام
|
| 114 |
+
text = re.sub(r'([،؛:!؟.])(?=\S)', r'\1 ', text)
|
| 115 |
+
|
| 116 |
+
# 8. إعادة الأرقام والكسور والتوقيت المحمية إلى أصلها
|
| 117 |
+
text = text.replace('٪TEMP_COMMA٪', ',').replace('٪TEMP_COLON٪', ':')
|
| 118 |
+
|
| 119 |
+
# 9. إلصاق علامات الترقيم بالكلمة السابقة لها مباشرة
|
| 120 |
+
text = re.sub(r'\s+([،؛:!؟.])', r'\1', text)
|
| 121 |
+
|
| 122 |
+
# 10. إزالة المسافات المتكررة الأفقية فقط (بدون لمس السطور الجديدة)
|
| 123 |
+
text = re.sub(r'[ \t]+', ' ', text).strip()
|
| 124 |
+
return text
|
| 125 |
+
|
| 126 |
+
def fix_punctuation(text):
|
| 127 |
+
"""معالجة الفقرة الواحدة الطويلة عبر تقسيمها لقطع غير متداخلة لمنع التكرار."""
|
| 128 |
+
words = text.split()
|
| 129 |
+
total_words = len(words)
|
| 130 |
+
|
| 131 |
+
# جعل حجم الخطوة مساوياً لحجم النافذة يمنع تكرار الكلمات تماماً
|
| 132 |
+
window_size = 50
|
| 133 |
+
stride = 50
|
| 134 |
+
|
| 135 |
+
if total_words <= window_size:
|
| 136 |
+
result = predict_chunk(text)
|
| 137 |
+
else:
|
| 138 |
+
segments_output = []
|
| 139 |
+
|
| 140 |
+
for i in range(0, total_words, stride):
|
| 141 |
+
chunk_words = words[i : i + window_size]
|
| 142 |
+
chunk_text = " ".join(chunk_words)
|
| 143 |
+
|
| 144 |
+
if not chunk_text.strip():
|
| 145 |
+
continue
|
| 146 |
+
|
| 147 |
+
processed_segment = predict_chunk(chunk_text).strip()
|
| 148 |
+
|
| 149 |
+
# مسح علامات الترقيم الناتجة عن القص الإجباري بين القطع
|
| 150 |
+
is_last_segment = (i + window_size) >= total_words
|
| 151 |
+
if not is_last_segment:
|
| 152 |
+
punctuation_marks = ".?!،؛:؟!"
|
| 153 |
+
if processed_segment and processed_segment[-1] in punctuation_marks:
|
| 154 |
+
# نمسح العلامة تماماً لأن السياق مستمر في القطعة اللي بعدها
|
| 155 |
+
processed_segment = processed_segment[:-1]
|
| 156 |
+
|
| 157 |
+
segments_output.append(processed_segment)
|
| 158 |
+
|
| 159 |
+
result = " ".join(segments_output)
|
| 160 |
+
|
| 161 |
+
# تنظيف المسافات الزائدة والتكرار إن وجد
|
| 162 |
+
result = re.sub(r'\s+', ' ', result).strip()
|
| 163 |
+
return result
|
| 164 |
+
|
| 165 |
+
def process_full_document(text):
|
| 166 |
+
if not text:
|
| 167 |
+
return text
|
| 168 |
+
|
| 169 |
+
# تقسيم بناءً على السطور الجديدة وتنظيف الأسطر الفارغة
|
| 170 |
+
paragraphs = [p.strip() for p in text.split('\n') if p.strip()]
|
| 171 |
+
processed_paragraphs = []
|
| 172 |
+
|
| 173 |
+
for paragraph in paragraphs:
|
| 174 |
+
# معالجة الفقرة المستقلة
|
| 175 |
+
punctuated_paragraph = fix_punctuation(paragraph)
|
| 176 |
+
cleaned_paragraph = arabic_postprocessing(punctuated_paragraph)
|
| 177 |
+
processed_paragraphs.append(cleaned_paragraph)
|
| 178 |
+
|
| 179 |
+
# الدمج بسطرين متباعدين لضمان الفصل البصري التام بين الفقرات
|
| 180 |
+
return "\n\n".join(processed_paragraphs)
|
archive/legacy_scripts/gradio Spelling.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
|
| 3 |
+
import re
|
| 4 |
+
from AraSpell import initialize
|
| 5 |
+
|
| 6 |
+
# تهيئة المصحح الإملائي وتحميل الموديل
|
| 7 |
+
sc = initialize(use_contextual=True)
|
| 8 |
+
|
| 9 |
+
import Levenshtein
|
| 10 |
+
|
| 11 |
+
def align_words(in_words, out_words):
|
| 12 |
+
n = len(in_words)
|
| 13 |
+
m = len(out_words)
|
| 14 |
+
dp = [[0] * (m + 1) for _ in range(n + 1)]
|
| 15 |
+
|
| 16 |
+
for i in range(1, n + 1):
|
| 17 |
+
dp[i][0] = dp[i-1][0] + len(in_words[i-1])
|
| 18 |
+
for j in range(1, m + 1):
|
| 19 |
+
dp[0][j] = dp[0][j-1] + len(out_words[j-1])
|
| 20 |
+
|
| 21 |
+
for i in range(1, n + 1):
|
| 22 |
+
for j in range(1, m + 1):
|
| 23 |
+
cost_replace = Levenshtein.distance(in_words[i-1], out_words[j-1])
|
| 24 |
+
dp[i][j] = min(
|
| 25 |
+
dp[i-1][j-1] + cost_replace,
|
| 26 |
+
dp[i-1][j] + len(in_words[i-1]),
|
| 27 |
+
dp[i][j-1] + len(out_words[j-1])
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
i, j = n, m
|
| 31 |
+
ops = []
|
| 32 |
+
while i > 0 or j > 0:
|
| 33 |
+
if i > 0 and j > 0:
|
| 34 |
+
cost_replace = Levenshtein.distance(in_words[i-1], out_words[j-1])
|
| 35 |
+
if dp[i][j] == dp[i-1][j-1] + cost_replace:
|
| 36 |
+
if cost_replace == 0:
|
| 37 |
+
ops.append(('equal', [in_words[i-1]], [out_words[j-1]]))
|
| 38 |
+
else:
|
| 39 |
+
ops.append(('replace', [in_words[i-1]], [out_words[j-1]]))
|
| 40 |
+
i -= 1
|
| 41 |
+
j -= 1
|
| 42 |
+
continue
|
| 43 |
+
if i > 0 and dp[i][j] == dp[i-1][j] + len(in_words[i-1]):
|
| 44 |
+
ops.append(('delete', [in_words[i-1]], []))
|
| 45 |
+
i -= 1
|
| 46 |
+
else:
|
| 47 |
+
ops.append(('insert', [], [out_words[j-1]]))
|
| 48 |
+
j -= 1
|
| 49 |
+
|
| 50 |
+
ops.reverse()
|
| 51 |
+
|
| 52 |
+
merged_ops = []
|
| 53 |
+
for op, in_w, out_w in ops:
|
| 54 |
+
if not merged_ops:
|
| 55 |
+
merged_ops.append([op, in_w, out_w])
|
| 56 |
+
continue
|
| 57 |
+
|
| 58 |
+
prev_op = merged_ops[-1][0]
|
| 59 |
+
if op != 'equal' and prev_op != 'equal' and not (op == 'replace' and prev_op == 'replace'):
|
| 60 |
+
merged_ops[-1][0] = 'replace'
|
| 61 |
+
merged_ops[-1][1].extend(in_w)
|
| 62 |
+
merged_ops[-1][2].extend(out_w)
|
| 63 |
+
else:
|
| 64 |
+
merged_ops.append([op, in_w, out_w])
|
| 65 |
+
|
| 66 |
+
return merged_ops
|
| 67 |
+
|
| 68 |
+
def generate_highlights(input_text):
|
| 69 |
+
if not input_text or not input_text.strip():
|
| 70 |
+
return [], {}
|
| 71 |
+
|
| 72 |
+
corrected_text = sc.correct(input_text)
|
| 73 |
+
|
| 74 |
+
in_words = input_text.split()
|
| 75 |
+
out_words = corrected_text.split()
|
| 76 |
+
|
| 77 |
+
ops = align_words(in_words, out_words)
|
| 78 |
+
|
| 79 |
+
highlight_list = []
|
| 80 |
+
suggestions_map = {}
|
| 81 |
+
|
| 82 |
+
idx = 0
|
| 83 |
+
for tag, in_w, out_w in ops:
|
| 84 |
+
if tag == 'equal':
|
| 85 |
+
for w in in_w:
|
| 86 |
+
highlight_list.append((w, None))
|
| 87 |
+
highlight_list.append((" ", None))
|
| 88 |
+
idx += 2
|
| 89 |
+
elif tag == 'replace' or tag == 'insert' or tag == 'delete':
|
| 90 |
+
in_phrase = " ".join(in_w) if in_w else "[ناقص]"
|
| 91 |
+
out_phrase = " ".join(out_w) if out_w else "(حذف الكلمة)"
|
| 92 |
+
|
| 93 |
+
highlight_list.append((in_phrase, " "))
|
| 94 |
+
sugs = [out_phrase]
|
| 95 |
+
|
| 96 |
+
if len(in_w) == 1 and len(out_w) == 1:
|
| 97 |
+
clean_w = re.sub(r'[^\w]', '', in_w[0])
|
| 98 |
+
try:
|
| 99 |
+
edit_cands = sc.edit_corrector.known(sc.edit_corrector.edits1(clean_w))
|
| 100 |
+
if edit_cands:
|
| 101 |
+
edit_cands = sorted(list(edit_cands), key=lambda x: sc.vocab_manager.get_frequency_rank(x))
|
| 102 |
+
for c in edit_cands:
|
| 103 |
+
if c not in sugs and len(sugs) < 3:
|
| 104 |
+
sugs.append(c)
|
| 105 |
+
except Exception:
|
| 106 |
+
pass
|
| 107 |
+
|
| 108 |
+
suggestions_map[idx] = sugs
|
| 109 |
+
highlight_list.append((" ", None))
|
| 110 |
+
idx += 2
|
| 111 |
+
|
| 112 |
+
if highlight_list and highlight_list[-1] == (" ", None):
|
| 113 |
+
highlight_list.pop()
|
| 114 |
+
|
| 115 |
+
return highlight_list, suggestions_map
|
| 116 |
+
|
| 117 |
+
# ==========================================
|
| 118 |
+
# تصميم واجهة المستخدم التفاعلية (Gradio Blocks)
|
| 119 |
+
# ==========================================
|
| 120 |
+
with gr.Blocks(theme=gr.themes.Soft(), css="""
|
| 121 |
+
.highlight-error { background-color: #ffcccc !important; border-radius: 4px; padding: 2px; }
|
| 122 |
+
.rtl-text { direction: rtl !important; text-align: right !important; }
|
| 123 |
+
""") as iface:
|
| 124 |
+
|
| 125 |
+
gr.Markdown("# 📝 AraSpell - المصحح الإملائي التفاعلي")
|
| 126 |
+
gr.Markdown("أدخل النص أدناه واضغط على **فحص النص**. سيقوم النظام بتلوين الأخطاء باللون الأحمر. **انقر على الكلمة الملونة** لتظهر لك خيارات التصحيح أسفلها!")
|
| 127 |
+
|
| 128 |
+
# متغيرات حالة (State) لحفظ البيانات خلف الكواليس
|
| 129 |
+
suggestions_state = gr.State({})
|
| 130 |
+
current_edit_index = gr.State(None)
|
| 131 |
+
highlight_list_state = gr.State([])
|
| 132 |
+
|
| 133 |
+
with gr.Row():
|
| 134 |
+
with gr.Column(scale=1):
|
| 135 |
+
input_box = gr.Textbox(lines=8, label="النص الأصلي", placeholder="أدخل النص العربي هنا...")
|
| 136 |
+
check_btn = gr.Button("🔍 فحص النص", variant="primary")
|
| 137 |
+
|
| 138 |
+
with gr.Column(scale=1):
|
| 139 |
+
output_highlights = gr.HighlightedText(
|
| 140 |
+
label="النتيجة (اضغط على الكلمات الملونة للتصحيح)",
|
| 141 |
+
combine_adjacent=False,
|
| 142 |
+
show_legend=False,
|
| 143 |
+
color_map={" ": "red"},
|
| 144 |
+
elem_classes="rtl-text"
|
| 145 |
+
)
|
| 146 |
+
|
| 147 |
+
# لوحة الاقتراحات (مخفية في البداية)
|
| 148 |
+
with gr.Group(visible=False) as suggestion_panel:
|
| 149 |
+
gr.Markdown("### 💡 اختر التصحيح المناسب:")
|
| 150 |
+
suggestion_radio = gr.Radio(choices=[], label="")
|
| 151 |
+
apply_btn = gr.Button("✅ تطبيق التصحيح", variant="secondary")
|
| 152 |
+
|
| 153 |
+
# 1. عند الضغط على فحص النص
|
| 154 |
+
def process_text(text):
|
| 155 |
+
h_list, s_map = generate_highlights(text)
|
| 156 |
+
# إرجاع: النص المظلل، قاموس الاقتراحات، قائمة التظليل (State)، وإخفاء لوحة الاقتراحات
|
| 157 |
+
return h_list, s_map, h_list, gr.update(visible=False)
|
| 158 |
+
|
| 159 |
+
check_btn.click(
|
| 160 |
+
fn=process_text,
|
| 161 |
+
inputs=[input_box],
|
| 162 |
+
outputs=[output_highlights, suggestions_state, highlight_list_state, suggestion_panel]
|
| 163 |
+
)
|
| 164 |
+
|
| 165 |
+
# 2. عند النقر على أي كلمة داخل النص المظلل
|
| 166 |
+
def on_highlight_click(evt: gr.SelectData, s_map):
|
| 167 |
+
index = evt.index
|
| 168 |
+
# معالجة مشكلة تحويل المفاتيح إلى نصوص (Strings) في Gradio State
|
| 169 |
+
if index in s_map:
|
| 170 |
+
choices = s_map[index]
|
| 171 |
+
elif str(index) in s_map:
|
| 172 |
+
choices = s_map[str(index)]
|
| 173 |
+
else:
|
| 174 |
+
# إخفاء اللوحة إذا ضغط على كلمة صحيحة
|
| 175 |
+
return gr.update(visible=False), gr.update(), None
|
| 176 |
+
|
| 177 |
+
# إظهار اللوحة وتحديث الخيارات
|
| 178 |
+
return gr.update(visible=True), gr.update(choices=choices, value=choices[0]), index
|
| 179 |
+
|
| 180 |
+
output_highlights.select(
|
| 181 |
+
fn=on_highlight_click,
|
| 182 |
+
inputs=[suggestions_state],
|
| 183 |
+
outputs=[suggestion_panel, suggestion_radio, current_edit_index]
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
# 3. عند اختيار اقتراح والضغط على "تطبيق"
|
| 187 |
+
def apply_correction(choice, edit_idx, h_list):
|
| 188 |
+
if edit_idx is not None and choice:
|
| 189 |
+
# تحديث الكلمة في قائمة التظليل (بدون إعادة تشغيل الموديل لتكون سريعة جداً)
|
| 190 |
+
if choice == "(حذف الكلمة)":
|
| 191 |
+
h_list[edit_idx] = ("", None)
|
| 192 |
+
else:
|
| 193 |
+
h_list[edit_idx] = (choice, None)
|
| 194 |
+
|
| 195 |
+
# إعادة بناء النص الجديد
|
| 196 |
+
new_text = "".join([t[0] for t in h_list])
|
| 197 |
+
|
| 198 |
+
# إرجاع: تحديث مربع الإدخال، التظليل الجديد، بقاء الاقتراحات كما هي، State الجديد، وإخفاء اللوحة
|
| 199 |
+
return new_text, h_list, gr.update(), h_list, gr.update(visible=False)
|
| 200 |
+
|
| 201 |
+
return gr.update(), gr.update(), gr.update(), gr.update(), gr.update()
|
| 202 |
+
|
| 203 |
+
apply_btn.click(
|
| 204 |
+
fn=apply_correction,
|
| 205 |
+
inputs=[suggestion_radio, current_edit_index, highlight_list_state],
|
| 206 |
+
outputs=[input_box, output_highlights, suggestions_state, highlight_list_state, suggestion_panel]
|
| 207 |
+
)
|
| 208 |
+
|
| 209 |
+
if __name__ == "__main__":
|
| 210 |
+
iface.launch()
|
archive/legacy_scripts/punctuation_rulesV2.py
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# PuncAra — Arabic Punctuation Restoration Rules
|
| 2 |
+
# Extracted from PuncAra.py — preprocessing + postprocessing + chunking logic.
|
| 3 |
+
# All classes are imported by punctuation_service.py.
|
| 4 |
+
|
| 5 |
+
import re
|
| 6 |
+
import logging
|
| 7 |
+
|
| 8 |
+
logger = logging.getLogger(__name__)
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def arabic_preprocessing(text: str) -> str:
|
| 12 |
+
"""Remove Arabic diacritics to normalize input for the model."""
|
| 13 |
+
arabic_diacritics = re.compile(r'[\u064B-\u0652]')
|
| 14 |
+
return re.sub(arabic_diacritics, '', text).strip()
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def arabic_postprocessing(text: str) -> str:
|
| 18 |
+
"""
|
| 19 |
+
Typographic cleanup and punctuation normalization after model inference.
|
| 20 |
+
Handles: bracket spacing, duplicate marks, chunk-join artifacts, etc.
|
| 21 |
+
"""
|
| 22 |
+
if not text:
|
| 23 |
+
return text
|
| 24 |
+
|
| 25 |
+
# 1. Protect numbers/fractions/time from incorrect conversion
|
| 26 |
+
text = re.sub(r'(?<=\d),(?=\d)', '٪TEMP_COMMA٪', text)
|
| 27 |
+
text = re.sub(r'(?<=\d):(?=\d)', '٪TEMP_COLON٪', text)
|
| 28 |
+
|
| 29 |
+
# 2. Arabize typographic marks
|
| 30 |
+
text = text.replace(',', '،').replace(';', '؛').replace('?', '؟')
|
| 31 |
+
|
| 32 |
+
# 3. Fix internal spacing for brackets and Arabic quotes
|
| 33 |
+
text = re.sub(r'\(\s+', '(', text)
|
| 34 |
+
text = re.sub(r'\s+\)', ')', text)
|
| 35 |
+
text = re.sub(r'\[\s+', '[', text)
|
| 36 |
+
text = re.sub(r'\s+\]', ']', text)
|
| 37 |
+
text = re.sub(r'«\s+', '«', text)
|
| 38 |
+
text = re.sub(r'\s+»', '»', text)
|
| 39 |
+
|
| 40 |
+
# 4. Remove repeated emotional marks (except ellipsis)
|
| 41 |
+
text = re.sub(r'([،؛:!؟])\1+', r'\1', text)
|
| 42 |
+
text = re.sub(r'\.{4,}', '...', text)
|
| 43 |
+
|
| 44 |
+
# 5. Fix chunk-join contradictions
|
| 45 |
+
text = re.sub(r'[،؛:]+([.!؟])', r'\1', text)
|
| 46 |
+
text = re.sub(r'،؛|؛،', '؛', text)
|
| 47 |
+
text = re.sub(r'([!؟])\.', r'\1', text)
|
| 48 |
+
|
| 49 |
+
# 6. Remove stray leading punctuation
|
| 50 |
+
text = re.sub(r'^[،؛:!؟. \t]+', '', text)
|
| 51 |
+
|
| 52 |
+
# 7. Ensure single space after punctuation before text
|
| 53 |
+
text = re.sub(r'([،؛:!؟.])(?=\S)', r'\1 ', text)
|
| 54 |
+
|
| 55 |
+
# 8. Restore protected numbers
|
| 56 |
+
text = text.replace('٪TEMP_COMMA٪', ',').replace('٪TEMP_COLON٪', ':')
|
| 57 |
+
|
| 58 |
+
# 9. Attach punctuation to preceding word
|
| 59 |
+
text = re.sub(r'\s+([،؛:!؟.])', r'\1', text)
|
| 60 |
+
|
| 61 |
+
# 10. Collapse horizontal spaces only
|
| 62 |
+
text = re.sub(r'[ \t]+', ' ', text).strip()
|
| 63 |
+
return text
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
# ══════════════════════════════════════════════════════════════════════════════
|
| 67 |
+
# PUNCTUATION SAFETY LAYER — Pipeline Hardening v3.3
|
| 68 |
+
# ══════════════════════════════════════════════════════════════════════════════
|
| 69 |
+
|
| 70 |
+
ARABIC_PUNCT_CHARS = set('.,،؛؟!:;?!')
|
| 71 |
+
MAX_PUNCT_DELTA = 3
|
| 72 |
+
MAX_PUNCT_DELTA_SHORT = 1 # Stricter cap for short texts (≤2 words)
|
| 73 |
+
MAX_PUNCT_RATIO = 0.5 # max punctuation delta per word (multi-word diffs)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def _normalize_for_comparison(text: str) -> str:
|
| 77 |
+
"""
|
| 78 |
+
Normalize Arabic for safe comparison.
|
| 79 |
+
Prevents false rejection from hamza/alef/ya variants.
|
| 80 |
+
"""
|
| 81 |
+
# Remove diacritics
|
| 82 |
+
text = re.sub(r'[\u064B-\u0652]', '', text)
|
| 83 |
+
# Fold hamza/alef variants: أ إ آ → ا
|
| 84 |
+
text = re.sub(r'[أإآ]', 'ا', text)
|
| 85 |
+
# Fold ya: ى → ي
|
| 86 |
+
text = text.replace('ى', 'ي')
|
| 87 |
+
# Fold ta marbuta: ة → ه (comparison only)
|
| 88 |
+
text = text.replace('ة', 'ه')
|
| 89 |
+
return text
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def validate_punctuation_diff(diff: dict, full_text: str = '') -> bool:
|
| 93 |
+
"""
|
| 94 |
+
Return True ONLY if the diff is a safe punctuation-only change.
|
| 95 |
+
|
| 96 |
+
ALLOWED:
|
| 97 |
+
- Inserting 1 punctuation mark (short text) or 1–3 (long text)
|
| 98 |
+
- Replacing one punctuation mark with another
|
| 99 |
+
- Adding terminal punctuation to any sentence (1+ words) that lacks it
|
| 100 |
+
|
| 101 |
+
REJECTED:
|
| 102 |
+
- Adding/deleting/duplicating Arabic words
|
| 103 |
+
- Rewriting phrases
|
| 104 |
+
- Excessive punctuation repetition (3+ consecutive identical)
|
| 105 |
+
- Punctuation spam: delta/word_count > 0.5 (multi-word diffs)
|
| 106 |
+
- Short text (≤2 words): delta > 1
|
| 107 |
+
- Any diff: delta > MAX_PUNCT_DELTA
|
| 108 |
+
- Adding terminal punctuation when text already ends with punct
|
| 109 |
+
"""
|
| 110 |
+
original = diff.get('original', '')
|
| 111 |
+
correction = diff.get('correction', '')
|
| 112 |
+
|
| 113 |
+
# ── Rule 0 (FIX-01, updated FIX-30): Reject terminal punctuation injection ──
|
| 114 |
+
# PuncAra-v1 unconditionally adds . or ؟ to every sentence.
|
| 115 |
+
# This rule catches the pattern: "word" → "word." / "word؟" / "word،"
|
| 116 |
+
# where the ONLY change is appending 1-2 terminal punctuation marks.
|
| 117 |
+
#
|
| 118 |
+
# FIX-30: Allow terminal punct for any text with at least 1 word that
|
| 119 |
+
# doesn't already end with punctuation. Only block for:
|
| 120 |
+
# - Text that already has terminal punctuation
|
| 121 |
+
# - Text ending in an ellipsis (...)
|
| 122 |
+
TERMINAL_PUNCT = set('.,،؛؟!:;?!')
|
| 123 |
+
orig_stripped = original.rstrip()
|
| 124 |
+
corr_stripped = correction.rstrip()
|
| 125 |
+
if orig_stripped and corr_stripped:
|
| 126 |
+
# Check if correction is just original + terminal punct
|
| 127 |
+
orig_alpha_r0 = re.sub(r'[.,،؛؟!:;?\s]', '', original)
|
| 128 |
+
corr_alpha_r0 = re.sub(r'[.,،؛؟!:;?\s]', '', correction)
|
| 129 |
+
if (_normalize_for_comparison(orig_alpha_r0) ==
|
| 130 |
+
_normalize_for_comparison(corr_alpha_r0)):
|
| 131 |
+
# Same word content — check if only terminal punct was added
|
| 132 |
+
orig_punct_end = sum(1 for c in original if c in TERMINAL_PUNCT)
|
| 133 |
+
corr_punct_end = sum(1 for c in correction if c in TERMINAL_PUNCT)
|
| 134 |
+
if corr_punct_end > orig_punct_end:
|
| 135 |
+
# Only adding punctuation — check if it's at the END (terminal)
|
| 136 |
+
orig_no_punct = re.sub(r'[.,،؛؟!:;?!]+$', '', original)
|
| 137 |
+
corr_no_punct = re.sub(r'[.,،؛؟!:;?!]+$', '', correction)
|
| 138 |
+
if _normalize_for_comparison(orig_no_punct.replace(' ', '')) == \
|
| 139 |
+
_normalize_for_comparison(corr_no_punct.replace(' ', '')):
|
| 140 |
+
# This is a pure terminal-punctuation addition.
|
| 141 |
+
# Decide whether to allow based on full text context.
|
| 142 |
+
# FIX-30: When full_text isn't provided (e.g. word-level diff
|
| 143 |
+
# calls), fall back to counting words in `original` instead of
|
| 144 |
+
# treating the count as 0 — that previously rejected every
|
| 145 |
+
# single-word diff regardless of the threshold below.
|
| 146 |
+
_word_count_source = full_text if full_text else original
|
| 147 |
+
_full_word_count = len(re.findall(
|
| 148 |
+
r'[\u0600-\u06FFa-zA-Z]+', _word_count_source
|
| 149 |
+
))
|
| 150 |
+
_full_already_has_terminal = bool(
|
| 151 |
+
re.search(r'[.،؛؟!?!][\s]*$', full_text)
|
| 152 |
+
) if full_text else False
|
| 153 |
+
# Also check for ellipsis (... at end)
|
| 154 |
+
_full_has_ellipsis = full_text.rstrip().endswith('...') if full_text else False
|
| 155 |
+
|
| 156 |
+
# FIX-30: Threshold lowered from 5 → 1. The docstring and the
|
| 157 |
+
# Phase 13 comment above both documented "3+ words" as the
|
| 158 |
+
# intended rule, while the code enforced 5 — and even single-
|
| 159 |
+
# word fragments ("اليوم" → "اليوم؟") are a legitimate terminal
|
| 160 |
+
# punctuation addition once we have at least one real word.
|
| 161 |
+
#
|
| 162 |
+
# FIX-31: Removed the FIX-29 exclamation/question-cue guard.
|
| 163 |
+
# It required an explicit interrogative word (هل/ماذا/متى/...)
|
| 164 |
+
# before allowing "؟" or "!" to be added, which rejected valid
|
| 165 |
+
# single-word terminal punctuation additions with no such cue
|
| 166 |
+
# (e.g. "اليوم" → "اليوم؟"). Terminal punctuation is now
|
| 167 |
+
# allowed regardless of cue words, as long as the remaining
|
| 168 |
+
# safety rules below (word count, duplicate terminal marks,
|
| 169 |
+
# ellipsis) still hold.
|
| 170 |
+
if _full_word_count >= 1 and not _full_already_has_terminal and not _full_has_ellipsis:
|
| 171 |
+
logger.info(
|
| 172 |
+
f"[PUNC-SAFETY] Allowed terminal punct for sentence "
|
| 173 |
+
f"({_full_word_count} words): "
|
| 174 |
+
f"'{original}' → '{correction}'"
|
| 175 |
+
)
|
| 176 |
+
# Fall through to remaining rules (don't return yet)
|
| 177 |
+
else:
|
| 178 |
+
# Already has terminal punct or ends in ellipsis → REJECT
|
| 179 |
+
logger.info(
|
| 180 |
+
f"[PUNC-SAFETY] TerminalPunctuationGuard triggered: removing trailing punctuation "
|
| 181 |
+
f"'{original}' → '{correction}'"
|
| 182 |
+
)
|
| 183 |
+
return False
|
| 184 |
+
|
| 185 |
+
# ── Rule 0b (Batch 4): Reject punct insertion when original has no punctuation ──
|
| 186 |
+
# If the original text has zero Arabic punctuation and the correction
|
| 187 |
+
# only adds commas/semicolons (not at the very end), it's overcorrection.
|
| 188 |
+
# This catches "already correct" texts that PuncAra sprinkles with commas.
|
| 189 |
+
orig_punct_count_r0b = sum(1 for c in original if c in ARABIC_PUNCT_CHARS)
|
| 190 |
+
if orig_punct_count_r0b == 0:
|
| 191 |
+
corr_punct_count_r0b = sum(1 for c in correction if c in ARABIC_PUNCT_CHARS)
|
| 192 |
+
if corr_punct_count_r0b > 0:
|
| 193 |
+
# Only allow if adding a single period/question at the very end
|
| 194 |
+
stripped_corr = correction.rstrip()
|
| 195 |
+
if stripped_corr and stripped_corr[-1] in '.؟?!':
|
| 196 |
+
# This is terminal punct (already handled by Rule 0)
|
| 197 |
+
pass
|
| 198 |
+
else:
|
| 199 |
+
# Mid-sentence punct insertion on a clean sentence → reject
|
| 200 |
+
logger.info(
|
| 201 |
+
f"[PUNC-SAFETY] Rejected mid-sentence punct insertion on clean text: "
|
| 202 |
+
f"'{original}' → '{correction}'"
|
| 203 |
+
)
|
| 204 |
+
return False
|
| 205 |
+
|
| 206 |
+
# ── Rule 0c (Batch 4 + FIX-26): Reject punctuation rearrangement/substitution ──
|
| 207 |
+
# When original already has punctuation and the correction merely MOVES,
|
| 208 |
+
# SUBSTITUTES, or STACKS marks (e.g., ، → : or ، → ؛ or ؟ → ؟!), reject.
|
| 209 |
+
# The PuncAra model should NOT replace or pile onto existing punctuation —
|
| 210 |
+
# a sentence that already ends with punctuation must never get a second
|
| 211 |
+
# mark added next to it.
|
| 212 |
+
orig_punct_count_r0c = sum(1 for c in original if c in ARABIC_PUNCT_CHARS)
|
| 213 |
+
corr_punct_count_r0c = sum(1 for c in correction if c in ARABIC_PUNCT_CHARS)
|
| 214 |
+
if orig_punct_count_r0c > 0 and corr_punct_count_r0c > 0:
|
| 215 |
+
# Both have punctuation — check if alpha content is the same
|
| 216 |
+
orig_alpha_r0c = re.sub(r'[.,،؛؟!:;?\s]', '', original)
|
| 217 |
+
corr_alpha_r0c = re.sub(r'[.,،؛؟!:;?\s]', '', correction)
|
| 218 |
+
if _normalize_for_comparison(orig_alpha_r0c) == _normalize_for_comparison(corr_alpha_r0c):
|
| 219 |
+
# Same word content, but punct changed — reject any punct modification,
|
| 220 |
+
# whether it's a substitution or an addition on top of existing punct.
|
| 221 |
+
logger.info(
|
| 222 |
+
f"[PUNC-SAFETY] Rejected punct substitution/stacking: "
|
| 223 |
+
f"'{original}' → '{correction}'"
|
| 224 |
+
)
|
| 225 |
+
return False
|
| 226 |
+
|
| 227 |
+
# ── Rule 1: Alphabetic content must be identical after normalization ──
|
| 228 |
+
orig_alpha = re.sub(r'[.,،؛؟!:;?\s]', '', original)
|
| 229 |
+
corr_alpha = re.sub(r'[.,،؛؟!:;?\s]', '', correction)
|
| 230 |
+
|
| 231 |
+
if _normalize_for_comparison(orig_alpha) != _normalize_for_comparison(corr_alpha):
|
| 232 |
+
return False
|
| 233 |
+
|
| 234 |
+
# ── Rule 2: Reject excessive repetition (3+ consecutive identical) ──
|
| 235 |
+
if re.search(r'([.,،؛؟!:;?])\1{2,}', correction):
|
| 236 |
+
return False
|
| 237 |
+
|
| 238 |
+
# ── Shared computation for Rules 3–5 ──
|
| 239 |
+
orig_punct_count = sum(1 for c in original if c in ARABIC_PUNCT_CHARS)
|
| 240 |
+
corr_punct_count = sum(1 for c in correction if c in ARABIC_PUNCT_CHARS)
|
| 241 |
+
punct_delta = max(0, corr_punct_count - orig_punct_count)
|
| 242 |
+
word_count = len(re.findall(r'[\u0600-\u06FFa-zA-Z]+', correction)) or 1
|
| 243 |
+
|
| 244 |
+
# ── Rule 3: Short-text hybrid cap (≤2 words → max 1 mark added) ──
|
| 245 |
+
if word_count <= 2 and punct_delta > MAX_PUNCT_DELTA_SHORT:
|
| 246 |
+
return False
|
| 247 |
+
|
| 248 |
+
# ── Rule 4: Ratio-based spam protection (multi-word diffs) ──
|
| 249 |
+
if word_count > 2 and punct_delta / word_count > MAX_PUNCT_RATIO:
|
| 250 |
+
return False
|
| 251 |
+
|
| 252 |
+
# ── Rule 5: Absolute delta cap ──
|
| 253 |
+
if punct_delta > MAX_PUNCT_DELTA:
|
| 254 |
+
return False
|
| 255 |
+
|
| 256 |
+
return True
|
| 257 |
+
|
archive/old_tests/deep_dive_expanded.json
ADDED
|
@@ -0,0 +1,1323 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"timestamp": "2026-06-20T19:17:40.208323+00:00",
|
| 3 |
+
"api_base": "https://bayan10-bayan-api.hf.space",
|
| 4 |
+
"health": {
|
| 5 |
+
"environment": "huggingface_spaces",
|
| 6 |
+
"mode": "hf_spaces_local",
|
| 7 |
+
"models": {
|
| 8 |
+
"autocomplete": true,
|
| 9 |
+
"grammar": true,
|
| 10 |
+
"punctuation": true,
|
| 11 |
+
"spelling": true,
|
| 12 |
+
"summarization": true
|
| 13 |
+
},
|
| 14 |
+
"note": "Free tier: summarization local, other models return input unchanged",
|
| 15 |
+
"status": "healthy",
|
| 16 |
+
"supabase": {
|
| 17 |
+
"configured": true
|
| 18 |
+
}
|
| 19 |
+
},
|
| 20 |
+
"cat1": [
|
| 21 |
+
{
|
| 22 |
+
"id": "C1-01",
|
| 23 |
+
"category": 1,
|
| 24 |
+
"input": "كانت الفتيات يلعبون في الحديقه",
|
| 25 |
+
"a_spelling": "كانت الفتيات يلعبون في الحديقة",
|
| 26 |
+
"a_grammar_on_original": "كانت الفتيات يلعبن في الحديقة",
|
| 27 |
+
"a_grammar_on_spell_corrected": "كانت الفتيات يلعبن في الحديقة",
|
| 28 |
+
"a_punctuation": "كانت الفتيات يلعبون في الحديقه.",
|
| 29 |
+
"grammar_diff_orig_vs_spell": [],
|
| 30 |
+
"b_corrected": "كانت الفتيات يلعبن في الحديقة.",
|
| 31 |
+
"b_suggestions": [
|
| 32 |
+
{
|
| 33 |
+
"alternatives": [],
|
| 34 |
+
"confidence": 1.0,
|
| 35 |
+
"correction": "يلعبن",
|
| 36 |
+
"end": 19,
|
| 37 |
+
"id": "48d68e84-19e8-4af0-b1ea-b7ae4c7427f4",
|
| 38 |
+
"locked": true,
|
| 39 |
+
"original": "يلعبون",
|
| 40 |
+
"priority": 3,
|
| 41 |
+
"start": 13,
|
| 42 |
+
"type": "grammar"
|
| 43 |
+
},
|
| 44 |
+
{
|
| 45 |
+
"alternatives": [],
|
| 46 |
+
"confidence": 0.8,
|
| 47 |
+
"correction": "الحديقة.",
|
| 48 |
+
"end": 30,
|
| 49 |
+
"id": "0012d2dc-08f6-44ad-a9d1-e07230045474",
|
| 50 |
+
"locked": true,
|
| 51 |
+
"original": "الحديقه",
|
| 52 |
+
"priority": 2,
|
| 53 |
+
"start": 23,
|
| 54 |
+
"type": "punctuation"
|
| 55 |
+
}
|
| 56 |
+
]
|
| 57 |
+
},
|
| 58 |
+
{
|
| 59 |
+
"id": "C1-02",
|
| 60 |
+
"category": 1,
|
| 61 |
+
"input": "ان الطالبات ذهبو الى الجامعه",
|
| 62 |
+
"a_spelling": "ان الط ابت ذهبوا إلى الجامعه",
|
| 63 |
+
"a_grammar_on_original": "إن الطالبات ذهبن إلى الجامعة",
|
| 64 |
+
"a_grammar_on_spell_corrected": "إن الطلاب ذهبوا إلى الجامعة",
|
| 65 |
+
"a_punctuation": "ان الطالبات ذهبو الى الجامعه!",
|
| 66 |
+
"grammar_diff_orig_vs_spell": [
|
| 67 |
+
{
|
| 68 |
+
"word_idx": 1,
|
| 69 |
+
"gram_on_orig": "الطالبات",
|
| 70 |
+
"gram_on_spell": "الطلاب"
|
| 71 |
+
},
|
| 72 |
+
{
|
| 73 |
+
"word_idx": 2,
|
| 74 |
+
"gram_on_orig": "ذهبن",
|
| 75 |
+
"gram_on_spell": "ذهبوا"
|
| 76 |
+
}
|
| 77 |
+
],
|
| 78 |
+
"b_corrected": "إن الطالبات ذهبن ذهبوا الجامعة.",
|
| 79 |
+
"b_suggestions": [
|
| 80 |
+
{
|
| 81 |
+
"alternatives": [],
|
| 82 |
+
"confidence": 1.0,
|
| 83 |
+
"correction": "ذهبن",
|
| 84 |
+
"end": 16,
|
| 85 |
+
"id": "8a21b71d-8a87-4e29-a828-4ce2b343ae2a",
|
| 86 |
+
"locked": true,
|
| 87 |
+
"original": "ذهبو",
|
| 88 |
+
"priority": 3,
|
| 89 |
+
"start": 12,
|
| 90 |
+
"type": "grammar"
|
| 91 |
+
},
|
| 92 |
+
{
|
| 93 |
+
"alternatives": [],
|
| 94 |
+
"confidence": 0.8,
|
| 95 |
+
"correction": "الجامعة.",
|
| 96 |
+
"end": 28,
|
| 97 |
+
"id": "0c9e127e-8e64-4cc4-a928-03651da1dd15",
|
| 98 |
+
"locked": true,
|
| 99 |
+
"original": "الجامعه",
|
| 100 |
+
"priority": 2,
|
| 101 |
+
"start": 21,
|
| 102 |
+
"type": "punctuation"
|
| 103 |
+
},
|
| 104 |
+
{
|
| 105 |
+
"alternatives": [],
|
| 106 |
+
"confidence": 1.0,
|
| 107 |
+
"correction": "إن",
|
| 108 |
+
"end": 2,
|
| 109 |
+
"id": "156cd1ce-37cd-4ade-888c-9e8d12a83b05",
|
| 110 |
+
"locked": true,
|
| 111 |
+
"original": "ان",
|
| 112 |
+
"priority": 1,
|
| 113 |
+
"start": 0,
|
| 114 |
+
"type": "spelling"
|
| 115 |
+
},
|
| 116 |
+
{
|
| 117 |
+
"alternatives": [
|
| 118 |
+
"ذهبوا",
|
| 119 |
+
"ال",
|
| 120 |
+
"الم",
|
| 121 |
+
"الى"
|
| 122 |
+
],
|
| 123 |
+
"confidence": 0.9,
|
| 124 |
+
"correction": "ذهبوا",
|
| 125 |
+
"end": 20,
|
| 126 |
+
"id": "836d7346-3ea8-4851-bc69-53df3e1ff6b4",
|
| 127 |
+
"locked": true,
|
| 128 |
+
"original": "الى",
|
| 129 |
+
"priority": 1,
|
| 130 |
+
"start": 17,
|
| 131 |
+
"type": "spelling"
|
| 132 |
+
}
|
| 133 |
+
]
|
| 134 |
+
},
|
| 135 |
+
{
|
| 136 |
+
"id": "C1-03",
|
| 137 |
+
"category": 1,
|
| 138 |
+
"input": "هذة المدينه جميله جدا ومناخها معتدل",
|
| 139 |
+
"a_spelling": "هذة المدينه جميله جدا ومناخها معتدل",
|
| 140 |
+
"a_grammar_on_original": "هذه المدينة جميلة جدا ومناخها معتدل",
|
| 141 |
+
"a_grammar_on_spell_corrected": "هذه المدينة جميلة جدا ومناخها معتدل",
|
| 142 |
+
"a_punctuation": "هذة المدينه جميله جدا ومناخها معتدل.",
|
| 143 |
+
"grammar_diff_orig_vs_spell": [],
|
| 144 |
+
"b_corrected": "هذه المدينة جميلة جدا ومناخها معتدل.",
|
| 145 |
+
"b_suggestions": [
|
| 146 |
+
{
|
| 147 |
+
"alternatives": [],
|
| 148 |
+
"confidence": 0.8,
|
| 149 |
+
"correction": "معتدل.",
|
| 150 |
+
"end": 35,
|
| 151 |
+
"id": "b960d84a-bcf6-40c3-976e-29632ad7f302",
|
| 152 |
+
"locked": true,
|
| 153 |
+
"original": "معتدل",
|
| 154 |
+
"priority": 2,
|
| 155 |
+
"start": 30,
|
| 156 |
+
"type": "punctuation"
|
| 157 |
+
},
|
| 158 |
+
{
|
| 159 |
+
"alternatives": [],
|
| 160 |
+
"confidence": 1.0,
|
| 161 |
+
"correction": "هذه المدينة جميلة",
|
| 162 |
+
"end": 17,
|
| 163 |
+
"id": "a89acaad-669a-4867-9ebd-6bd5cbfad2ea",
|
| 164 |
+
"locked": true,
|
| 165 |
+
"original": "هذة المدينه جميله",
|
| 166 |
+
"priority": 1,
|
| 167 |
+
"start": 0,
|
| 168 |
+
"type": "spelling"
|
| 169 |
+
}
|
| 170 |
+
]
|
| 171 |
+
},
|
| 172 |
+
{
|
| 173 |
+
"id": "C1-04",
|
| 174 |
+
"category": 1,
|
| 175 |
+
"input": "الطلاب اجتهدو في دراستهم وحققو نتائج ممتازه",
|
| 176 |
+
"a_spelling": "الطلاب الاجتهادوا في دراستهم وحققوا نتائج ممتازه",
|
| 177 |
+
"a_grammar_on_original": "الطلاب اجتهدو في دراستهم وحققوا نتائج ممتازة",
|
| 178 |
+
"a_grammar_on_spell_corrected": "الطلاب اجتهدوا في دراستهم وحققوا نتائج ممتازة",
|
| 179 |
+
"a_punctuation": "الطلاب اجتهدو في دراستهم وحققو نتائج ممتازه.",
|
| 180 |
+
"grammar_diff_orig_vs_spell": [
|
| 181 |
+
{
|
| 182 |
+
"word_idx": 1,
|
| 183 |
+
"gram_on_orig": "اجتهدو",
|
| 184 |
+
"gram_on_spell": "اجتهدوا"
|
| 185 |
+
}
|
| 186 |
+
],
|
| 187 |
+
"b_corrected": "الطلاب اجتهدو في دراستهم وحققوا نتائج ممتازة.",
|
| 188 |
+
"b_suggestions": [
|
| 189 |
+
{
|
| 190 |
+
"alternatives": [],
|
| 191 |
+
"confidence": 0.8,
|
| 192 |
+
"correction": "ممتازة.",
|
| 193 |
+
"end": 43,
|
| 194 |
+
"id": "19a6216b-b7a1-4c0f-acd9-5c698617443b",
|
| 195 |
+
"locked": true,
|
| 196 |
+
"original": "ممتازه",
|
| 197 |
+
"priority": 2,
|
| 198 |
+
"start": 37,
|
| 199 |
+
"type": "punctuation"
|
| 200 |
+
},
|
| 201 |
+
{
|
| 202 |
+
"alternatives": [
|
| 203 |
+
"وحققوا",
|
| 204 |
+
"وحقوق",
|
| 205 |
+
"وحقق",
|
| 206 |
+
"وحققو"
|
| 207 |
+
],
|
| 208 |
+
"confidence": 0.9,
|
| 209 |
+
"correction": "وحققوا",
|
| 210 |
+
"end": 30,
|
| 211 |
+
"id": "cdcf61bd-8983-44c8-9c3d-8792dc8027c3",
|
| 212 |
+
"locked": true,
|
| 213 |
+
"original": "وحققو",
|
| 214 |
+
"priority": 1,
|
| 215 |
+
"start": 25,
|
| 216 |
+
"type": "spelling"
|
| 217 |
+
}
|
| 218 |
+
]
|
| 219 |
+
},
|
| 220 |
+
{
|
| 221 |
+
"id": "C1-05",
|
| 222 |
+
"category": 1,
|
| 223 |
+
"input": "ذهب الولد الى المكتبه وقرا كتاب مفيد",
|
| 224 |
+
"a_spelling": "ذهب الولد إلى المكتبة وقرا كتاب مفيد",
|
| 225 |
+
"a_grammar_on_original": "ذهب الولد إلى المكتبة وقرا كتابا مفيدا",
|
| 226 |
+
"a_grammar_on_spell_corrected": "ذهب الولد إلى المكتبة وقرا كتابا مفيدا",
|
| 227 |
+
"a_punctuation": "ذهب الولد الى المكتبه وقرا، كتاب مفيد",
|
| 228 |
+
"grammar_diff_orig_vs_spell": [],
|
| 229 |
+
"b_corrected": "ذهب الولد إلى المكتبة وقرا كتابا مفيدا",
|
| 230 |
+
"b_suggestions": [
|
| 231 |
+
{
|
| 232 |
+
"alternatives": [],
|
| 233 |
+
"confidence": 1.0,
|
| 234 |
+
"correction": "كتابا مفيدا",
|
| 235 |
+
"end": 36,
|
| 236 |
+
"id": "ede929d9-8112-4d65-a460-7a44cee535f5",
|
| 237 |
+
"locked": true,
|
| 238 |
+
"original": "كتاب مفيد",
|
| 239 |
+
"priority": 3,
|
| 240 |
+
"start": 27,
|
| 241 |
+
"type": "grammar"
|
| 242 |
+
},
|
| 243 |
+
{
|
| 244 |
+
"alternatives": [
|
| 245 |
+
"إلى",
|
| 246 |
+
"ال",
|
| 247 |
+
"الم",
|
| 248 |
+
"الى"
|
| 249 |
+
],
|
| 250 |
+
"confidence": 0.9,
|
| 251 |
+
"correction": "إلى",
|
| 252 |
+
"end": 13,
|
| 253 |
+
"id": "affb4882-0466-4184-93e7-fb3463132a83",
|
| 254 |
+
"locked": true,
|
| 255 |
+
"original": "الى",
|
| 256 |
+
"priority": 1,
|
| 257 |
+
"start": 10,
|
| 258 |
+
"type": "spelling"
|
| 259 |
+
},
|
| 260 |
+
{
|
| 261 |
+
"alternatives": [
|
| 262 |
+
"المكتبة",
|
| 263 |
+
"المكتب",
|
| 264 |
+
"المشتبه",
|
| 265 |
+
"المكتبه"
|
| 266 |
+
],
|
| 267 |
+
"confidence": 0.9,
|
| 268 |
+
"correction": "المكتبة",
|
| 269 |
+
"end": 21,
|
| 270 |
+
"id": "3f73b099-d428-4c05-98f8-138fb1d83c54",
|
| 271 |
+
"locked": true,
|
| 272 |
+
"original": "المكتبه",
|
| 273 |
+
"priority": 1,
|
| 274 |
+
"start": 14,
|
| 275 |
+
"type": "spelling"
|
| 276 |
+
}
|
| 277 |
+
]
|
| 278 |
+
}
|
| 279 |
+
],
|
| 280 |
+
"cat7": [
|
| 281 |
+
{
|
| 282 |
+
"id": "C7-01",
|
| 283 |
+
"category": 7,
|
| 284 |
+
"input": "ذهب الولد الى المدرسه وقابل المعلمه واخذ الكتاب",
|
| 285 |
+
"desc": "3-stage chain: spelling الى→إلى, grammar المدرسه→المدرسة, punc adds marks",
|
| 286 |
+
"a_spelling": "ذهب الولد إلى المدرسه وقابل المعلمه وأخذ الكتاب",
|
| 287 |
+
"a_grammar": "ذهب الولد إلى المدرسة وقابل المعلمة وأخذ الكتاب",
|
| 288 |
+
"a_punc": "ذهب الولد الى المدرسه وقابل، المعلمه واخذ الكتاب،",
|
| 289 |
+
"b_corrected": "ذهب الولد إلى المدرسة وقابل المعلمة وأخ�� الكتاب.",
|
| 290 |
+
"b_suggestions": [
|
| 291 |
+
{
|
| 292 |
+
"alternatives": [],
|
| 293 |
+
"confidence": 0.8,
|
| 294 |
+
"correction": "الكتاب.",
|
| 295 |
+
"end": 47,
|
| 296 |
+
"id": "3e740303-1dcd-42ec-bd6c-7f0af8069e44",
|
| 297 |
+
"locked": true,
|
| 298 |
+
"original": "الكتاب",
|
| 299 |
+
"priority": 2,
|
| 300 |
+
"start": 41,
|
| 301 |
+
"type": "punctuation"
|
| 302 |
+
},
|
| 303 |
+
{
|
| 304 |
+
"alternatives": [],
|
| 305 |
+
"confidence": 1.0,
|
| 306 |
+
"correction": "المدرسة",
|
| 307 |
+
"end": 21,
|
| 308 |
+
"id": "84f953ae-2d0a-4e99-a07d-7d35638ba843",
|
| 309 |
+
"locked": true,
|
| 310 |
+
"original": "المدرسه",
|
| 311 |
+
"priority": 1,
|
| 312 |
+
"start": 14,
|
| 313 |
+
"type": "spelling"
|
| 314 |
+
},
|
| 315 |
+
{
|
| 316 |
+
"alternatives": [],
|
| 317 |
+
"confidence": 1.0,
|
| 318 |
+
"correction": "المعلمة",
|
| 319 |
+
"end": 35,
|
| 320 |
+
"id": "2e793ab2-29fc-454b-8490-ca7cfdfe4404",
|
| 321 |
+
"locked": true,
|
| 322 |
+
"original": "المعلمه",
|
| 323 |
+
"priority": 1,
|
| 324 |
+
"start": 28,
|
| 325 |
+
"type": "spelling"
|
| 326 |
+
},
|
| 327 |
+
{
|
| 328 |
+
"alternatives": [
|
| 329 |
+
"إلى",
|
| 330 |
+
"ال",
|
| 331 |
+
"الم",
|
| 332 |
+
"الى"
|
| 333 |
+
],
|
| 334 |
+
"confidence": 0.9,
|
| 335 |
+
"correction": "إلى",
|
| 336 |
+
"end": 13,
|
| 337 |
+
"id": "d1b951c7-f94e-4941-986c-8ce5fa51bab0",
|
| 338 |
+
"locked": true,
|
| 339 |
+
"original": "الى",
|
| 340 |
+
"priority": 1,
|
| 341 |
+
"start": 10,
|
| 342 |
+
"type": "spelling"
|
| 343 |
+
},
|
| 344 |
+
{
|
| 345 |
+
"alternatives": [
|
| 346 |
+
"وأخذ",
|
| 347 |
+
"والذ",
|
| 348 |
+
"واخت",
|
| 349 |
+
"واخذ"
|
| 350 |
+
],
|
| 351 |
+
"confidence": 0.9,
|
| 352 |
+
"correction": "وأخذ",
|
| 353 |
+
"end": 40,
|
| 354 |
+
"id": "c024f6ae-4740-4d87-9a0d-95818699e4f3",
|
| 355 |
+
"locked": true,
|
| 356 |
+
"original": "واخذ",
|
| 357 |
+
"priority": 1,
|
| 358 |
+
"start": 36,
|
| 359 |
+
"type": "spelling"
|
| 360 |
+
}
|
| 361 |
+
],
|
| 362 |
+
"b_suggestion_count": 5,
|
| 363 |
+
"overlapping_suggestions": []
|
| 364 |
+
},
|
| 365 |
+
{
|
| 366 |
+
"id": "C7-02",
|
| 367 |
+
"category": 7,
|
| 368 |
+
"input": "كانت البنات يلعبون في الحديقه الجميله وفجأه سقطت احداهن",
|
| 369 |
+
"desc": "Multiple overlapping corrections across all stages",
|
| 370 |
+
"a_spelling": "كانت البنات يلعبون في الحديقه الجميله وفجأه سقطت احداهن",
|
| 371 |
+
"a_grammar": "كانت البنات يلعبن في الحديقة الجميلة وفجأة سقطت أختهن",
|
| 372 |
+
"a_punc": "كانت البنات يلعبون في الحديقه الجميله وفجأه، سقطت احداهن",
|
| 373 |
+
"b_corrected": "كانت البنات يلعبن في الحديقة الجميلة وفجأة سقطت أختهن.",
|
| 374 |
+
"b_suggestions": [
|
| 375 |
+
{
|
| 376 |
+
"alternatives": [],
|
| 377 |
+
"confidence": 1.0,
|
| 378 |
+
"correction": "يلعبن",
|
| 379 |
+
"end": 18,
|
| 380 |
+
"id": "2d1e033f-60d9-4921-bb65-d13b890f44d4",
|
| 381 |
+
"locked": true,
|
| 382 |
+
"original": "يلعبون",
|
| 383 |
+
"priority": 3,
|
| 384 |
+
"start": 12,
|
| 385 |
+
"type": "grammar"
|
| 386 |
+
},
|
| 387 |
+
{
|
| 388 |
+
"alternatives": [],
|
| 389 |
+
"confidence": 0.8,
|
| 390 |
+
"correction": "أختهن.",
|
| 391 |
+
"end": 55,
|
| 392 |
+
"id": "409bca6e-33d5-4339-8d75-d8dc1e3a8cea",
|
| 393 |
+
"locked": true,
|
| 394 |
+
"original": "احداهن",
|
| 395 |
+
"priority": 2,
|
| 396 |
+
"start": 49,
|
| 397 |
+
"type": "punctuation"
|
| 398 |
+
},
|
| 399 |
+
{
|
| 400 |
+
"alternatives": [],
|
| 401 |
+
"confidence": 1.0,
|
| 402 |
+
"correction": "الحديقة الجميلة وفجأة",
|
| 403 |
+
"end": 43,
|
| 404 |
+
"id": "ac5b5008-0563-4168-99aa-46c8de59d1d1",
|
| 405 |
+
"locked": true,
|
| 406 |
+
"original": "الحديقه الجميله وفجأه",
|
| 407 |
+
"priority": 1,
|
| 408 |
+
"start": 22,
|
| 409 |
+
"type": "spelling"
|
| 410 |
+
}
|
| 411 |
+
],
|
| 412 |
+
"b_suggestion_count": 3,
|
| 413 |
+
"overlapping_suggestions": []
|
| 414 |
+
},
|
| 415 |
+
{
|
| 416 |
+
"id": "C7-03",
|
| 417 |
+
"category": 7,
|
| 418 |
+
"input": "ان الذكاء الاصطناعي يلعب دورا هاما في تطوير التكنولوجيا الحديثه ولذالك يجب الاهتمام بة",
|
| 419 |
+
"desc": "Long sentence with corrections from all 3 stages",
|
| 420 |
+
"a_spelling": "أن الذكاء الاصطناعي يلعب دورا هاما في تطوير التكنولوجيا الحديثة ولذا ذلك يجب الاهتمام بة",
|
| 421 |
+
"a_grammar": "إن الذكاء الاصطناعي يؤدي دورا مهمّا في تطوير التكنولوجيا الحديثة ولذلك يجب الاهتمام به",
|
| 422 |
+
"a_punc": "ان الذكاء الاصطناعي يلعب دورا هاما في تطوير التكنولوجيا الحديثه ولذالك؛ يجب الاهتمام بة",
|
| 423 |
+
"b_corrected": "أن الذكاء الاصطناعي يلعب دورا هاما في تطوير التكنولوجيا الحديثة ولذا ذلك يجب الاهتمام به",
|
| 424 |
+
"b_suggestions": [
|
| 425 |
+
{
|
| 426 |
+
"alternatives": [],
|
| 427 |
+
"confidence": 1.0,
|
| 428 |
+
"correction": "به",
|
| 429 |
+
"end": 86,
|
| 430 |
+
"id": "0a44f7d9-1554-428c-809a-706631ffebdd",
|
| 431 |
+
"locked": true,
|
| 432 |
+
"original": "بة",
|
| 433 |
+
"priority": 1,
|
| 434 |
+
"start": 84,
|
| 435 |
+
"type": "spelling"
|
| 436 |
+
},
|
| 437 |
+
{
|
| 438 |
+
"alternatives": [
|
| 439 |
+
"أن",
|
| 440 |
+
"ال",
|
| 441 |
+
"من",
|
| 442 |
+
"ان"
|
| 443 |
+
],
|
| 444 |
+
"confidence": 0.9,
|
| 445 |
+
"correction": "أن",
|
| 446 |
+
"end": 2,
|
| 447 |
+
"id": "512b3c30-eadb-40fc-a481-1d4cd0909459",
|
| 448 |
+
"locked": true,
|
| 449 |
+
"original": "ان",
|
| 450 |
+
"priority": 1,
|
| 451 |
+
"start": 0,
|
| 452 |
+
"type": "spelling"
|
| 453 |
+
},
|
| 454 |
+
{
|
| 455 |
+
"alternatives": [
|
| 456 |
+
"الحديثة",
|
| 457 |
+
"الحديث",
|
| 458 |
+
"الحديثي",
|
| 459 |
+
"الحديثه"
|
| 460 |
+
],
|
| 461 |
+
"confidence": 0.9,
|
| 462 |
+
"correction": "الحديثة",
|
| 463 |
+
"end": 63,
|
| 464 |
+
"id": "9eb1f571-7852-4813-975d-f7cd79102ec8",
|
| 465 |
+
"locked": true,
|
| 466 |
+
"original": "الحديثه",
|
| 467 |
+
"priority": 1,
|
| 468 |
+
"start": 56,
|
| 469 |
+
"type": "spelling"
|
| 470 |
+
},
|
| 471 |
+
{
|
| 472 |
+
"alternatives": [
|
| 473 |
+
"ولذا ذلك",
|
| 474 |
+
"ولذالك"
|
| 475 |
+
],
|
| 476 |
+
"confidence": 0.85,
|
| 477 |
+
"correction": "ولذا ذلك",
|
| 478 |
+
"end": 70,
|
| 479 |
+
"id": "266db5db-ed2a-49be-accb-76db9e07697f",
|
| 480 |
+
"locked": true,
|
| 481 |
+
"original": "ولذالك",
|
| 482 |
+
"priority": 1,
|
| 483 |
+
"start": 64,
|
| 484 |
+
"type": "spelling"
|
| 485 |
+
}
|
| 486 |
+
],
|
| 487 |
+
"b_suggestion_count": 4,
|
| 488 |
+
"overlapping_suggestions": []
|
| 489 |
+
},
|
| 490 |
+
{
|
| 491 |
+
"id": "C7-04",
|
| 492 |
+
"category": 7,
|
| 493 |
+
"input": "هذة المدينه جميله جدا ومناخها معتدل طوال العام وسكانها طيبون جدا",
|
| 494 |
+
"desc": "Multiple ه→ة fixes: does grammar lock prevent punc from adding marks near those words?",
|
| 495 |
+
"a_spelling": "هذة المدينه جميله جدا ومناخها معتدل طوال العام وسكان طيبون جدا",
|
| 496 |
+
"a_grammar": "هذه المدينة جميلة جدا ومناخها معتدل طوال العام وسكانها طيبون جدا",
|
| 497 |
+
"a_punc": "هذة المدينه جميله جدا ومناخها معتدل طوال العام وسكانها طيبون جدا.",
|
| 498 |
+
"b_corrected": "هذه المدينة جميلة جدا ومناخها معتدل طوال العام وسكانها طيبون جدا.",
|
| 499 |
+
"b_suggestions": [
|
| 500 |
+
{
|
| 501 |
+
"alternatives": [],
|
| 502 |
+
"confidence": 0.8,
|
| 503 |
+
"correction": "جدا.",
|
| 504 |
+
"end": 64,
|
| 505 |
+
"id": "0dd9d98a-f146-492b-87c7-dba4913bdfd4",
|
| 506 |
+
"locked": true,
|
| 507 |
+
"original": "جدا",
|
| 508 |
+
"priority": 2,
|
| 509 |
+
"start": 61,
|
| 510 |
+
"type": "punctuation"
|
| 511 |
+
},
|
| 512 |
+
{
|
| 513 |
+
"alternatives": [],
|
| 514 |
+
"confidence": 1.0,
|
| 515 |
+
"correction": "هذه المدينة جميلة",
|
| 516 |
+
"end": 17,
|
| 517 |
+
"id": "e12aa2f9-63d7-4f10-a128-13b0b1bbee9a",
|
| 518 |
+
"locked": true,
|
| 519 |
+
"original": "هذة المدينه جميله",
|
| 520 |
+
"priority": 1,
|
| 521 |
+
"start": 0,
|
| 522 |
+
"type": "spelling"
|
| 523 |
+
}
|
| 524 |
+
],
|
| 525 |
+
"b_suggestion_count": 2,
|
| 526 |
+
"overlapping_suggestions": []
|
| 527 |
+
},
|
| 528 |
+
{
|
| 529 |
+
"id": "C7-05",
|
| 530 |
+
"category": 7,
|
| 531 |
+
"input": "الطلاب اللذين اجتهدو في دراستهم حققو نتائج ممتازه في الأمتحانات الصعبه",
|
| 532 |
+
"desc": "Heavy corrections needed across stages",
|
| 533 |
+
"a_spelling": "الطلاب اللذين اجتهد في دراستهم حقوق نتائج ممتازه في الأمتحانات الصعبه",
|
| 534 |
+
"a_grammar": "الطلاب الذين اجتهدو في دراستهم حققوا نتائج ممتازة في الامتحانات الصعبة",
|
| 535 |
+
"a_punc": "الطلاب اللذين اجتهدو في دراستهم حققو نتائج ممتازه في الأمتحانات الصعبه.",
|
| 536 |
+
"b_corrected": "الطلاب اللذين اجتهد في دراستهم حققوا نتائج ممتازة في الأمتحانات الصعبة.",
|
| 537 |
+
"b_suggestions": [
|
| 538 |
+
{
|
| 539 |
+
"alternatives": [],
|
| 540 |
+
"confidence": 1.0,
|
| 541 |
+
"correction": "حققوا",
|
| 542 |
+
"end": 36,
|
| 543 |
+
"id": "b2ae6a56-0879-4572-837f-875895de9020",
|
| 544 |
+
"locked": true,
|
| 545 |
+
"original": "حققو",
|
| 546 |
+
"priority": 3,
|
| 547 |
+
"start": 32,
|
| 548 |
+
"type": "grammar"
|
| 549 |
+
},
|
| 550 |
+
{
|
| 551 |
+
"alternatives": [],
|
| 552 |
+
"confidence": 0.8,
|
| 553 |
+
"correction": "الصعبة.",
|
| 554 |
+
"end": 70,
|
| 555 |
+
"id": "b075d0de-0e6d-4d88-a897-8e79e1845116",
|
| 556 |
+
"locked": true,
|
| 557 |
+
"original": "الصعبه",
|
| 558 |
+
"priority": 2,
|
| 559 |
+
"start": 64,
|
| 560 |
+
"type": "punctuation"
|
| 561 |
+
},
|
| 562 |
+
{
|
| 563 |
+
"alternatives": [],
|
| 564 |
+
"confidence": 1.0,
|
| 565 |
+
"correction": "ممتازة",
|
| 566 |
+
"end": 49,
|
| 567 |
+
"id": "ee092cbc-bc6f-41f0-9c98-7cc2edeee671",
|
| 568 |
+
"locked": true,
|
| 569 |
+
"original": "ممتازه",
|
| 570 |
+
"priority": 1,
|
| 571 |
+
"start": 43,
|
| 572 |
+
"type": "spelling"
|
| 573 |
+
},
|
| 574 |
+
{
|
| 575 |
+
"alternatives": [
|
| 576 |
+
"اجتهد",
|
| 577 |
+
"اجتهدو"
|
| 578 |
+
],
|
| 579 |
+
"confidence": 0.9,
|
| 580 |
+
"correction": "اجتهد",
|
| 581 |
+
"end": 20,
|
| 582 |
+
"id": "97a2307e-5a5a-4668-a4fe-052bbf86c4d9",
|
| 583 |
+
"locked": true,
|
| 584 |
+
"original": "اجتهدو",
|
| 585 |
+
"priority": 1,
|
| 586 |
+
"start": 14,
|
| 587 |
+
"type": "spelling"
|
| 588 |
+
}
|
| 589 |
+
],
|
| 590 |
+
"b_suggestion_count": 4,
|
| 591 |
+
"overlapping_suggestions": []
|
| 592 |
+
}
|
| 593 |
+
],
|
| 594 |
+
"cat8x": [
|
| 595 |
+
{
|
| 596 |
+
"id": "C8X-مدرسة-al",
|
| 597 |
+
"category": 8,
|
| 598 |
+
"input": "المدرسة",
|
| 599 |
+
"root": "مدرسة",
|
| 600 |
+
"prefix_combo": "al",
|
| 601 |
+
"track_a_spelling": "المدرسة",
|
| 602 |
+
"changed": false
|
| 603 |
+
},
|
| 604 |
+
{
|
| 605 |
+
"id": "C8X-مدرسة-wal",
|
| 606 |
+
"category": 8,
|
| 607 |
+
"input": "والمدرسة",
|
| 608 |
+
"root": "مدرسة",
|
| 609 |
+
"prefix_combo": "wal",
|
| 610 |
+
"track_a_spelling": "والمدرسة في المدرسة",
|
| 611 |
+
"changed": true
|
| 612 |
+
},
|
| 613 |
+
{
|
| 614 |
+
"id": "C8X-مدرسة-bal",
|
| 615 |
+
"category": 8,
|
| 616 |
+
"input": "بالمدرسة",
|
| 617 |
+
"root": "مدرسة",
|
| 618 |
+
"prefix_combo": "bal",
|
| 619 |
+
"track_a_spelling": "بالمدرسة في المدرسة",
|
| 620 |
+
"changed": true
|
| 621 |
+
},
|
| 622 |
+
{
|
| 623 |
+
"id": "C8X-مدرسة-lal",
|
| 624 |
+
"category": 8,
|
| 625 |
+
"input": "للمدرسة",
|
| 626 |
+
"root": "مدرسة",
|
| 627 |
+
"prefix_combo": "lal",
|
| 628 |
+
"track_a_spelling": "للمدرسة",
|
| 629 |
+
"changed": false
|
| 630 |
+
},
|
| 631 |
+
{
|
| 632 |
+
"id": "C8X-شمس-al",
|
| 633 |
+
"category": 8,
|
| 634 |
+
"input": "الشمس",
|
| 635 |
+
"root": "شمس",
|
| 636 |
+
"prefix_combo": "al",
|
| 637 |
+
"track_a_spelling": "الشمس",
|
| 638 |
+
"changed": false
|
| 639 |
+
},
|
| 640 |
+
{
|
| 641 |
+
"id": "C8X-شمس-wal",
|
| 642 |
+
"category": 8,
|
| 643 |
+
"input": "والشمس",
|
| 644 |
+
"root": "شمس",
|
| 645 |
+
"prefix_combo": "wal",
|
| 646 |
+
"track_a_spelling": "والشمس والشمس",
|
| 647 |
+
"changed": true
|
| 648 |
+
},
|
| 649 |
+
{
|
| 650 |
+
"id": "C8X-شمس-bal",
|
| 651 |
+
"category": 8,
|
| 652 |
+
"input": "بالشمس",
|
| 653 |
+
"root": "شمس",
|
| 654 |
+
"prefix_combo": "bal",
|
| 655 |
+
"track_a_spelling": "الشمس",
|
| 656 |
+
"changed": true
|
| 657 |
+
},
|
| 658 |
+
{
|
| 659 |
+
"id": "C8X-شمس-lal",
|
| 660 |
+
"category": 8,
|
| 661 |
+
"input": "للشمس",
|
| 662 |
+
"root": "شمس",
|
| 663 |
+
"prefix_combo": "lal",
|
| 664 |
+
"track_a_spelling": "للشمس",
|
| 665 |
+
"changed": false
|
| 666 |
+
},
|
| 667 |
+
{
|
| 668 |
+
"id": "C8X-أمة-al",
|
| 669 |
+
"category": 8,
|
| 670 |
+
"input": "الأمة",
|
| 671 |
+
"root": "أمة",
|
| 672 |
+
"prefix_combo": "al",
|
| 673 |
+
"track_a_spelling": "الأمة",
|
| 674 |
+
"changed": false
|
| 675 |
+
},
|
| 676 |
+
{
|
| 677 |
+
"id": "C8X-أمة-wal",
|
| 678 |
+
"category": 8,
|
| 679 |
+
"input": "والأمة",
|
| 680 |
+
"root": "أمة",
|
| 681 |
+
"prefix_combo": "wal",
|
| 682 |
+
"track_a_spelling": "والأمة الأمة",
|
| 683 |
+
"changed": true
|
| 684 |
+
},
|
| 685 |
+
{
|
| 686 |
+
"id": "C8X-أمة-bal",
|
| 687 |
+
"category": 8,
|
| 688 |
+
"input": "بالأمة",
|
| 689 |
+
"root": "أمة",
|
| 690 |
+
"prefix_combo": "bal",
|
| 691 |
+
"track_a_spelling": "الأمة",
|
| 692 |
+
"changed": true
|
| 693 |
+
},
|
| 694 |
+
{
|
| 695 |
+
"id": "C8X-أمة-lal",
|
| 696 |
+
"category": 8,
|
| 697 |
+
"input": "للأمة",
|
| 698 |
+
"root": "أمة",
|
| 699 |
+
"prefix_combo": "lal",
|
| 700 |
+
"track_a_spelling": "للأمة",
|
| 701 |
+
"changed": false
|
| 702 |
+
},
|
| 703 |
+
{
|
| 704 |
+
"id": "C8X-نافذة-al",
|
| 705 |
+
"category": 8,
|
| 706 |
+
"input": "النافذة",
|
| 707 |
+
"root": "نافذة",
|
| 708 |
+
"prefix_combo": "al",
|
| 709 |
+
"track_a_spelling": "النافذة",
|
| 710 |
+
"changed": false
|
| 711 |
+
},
|
| 712 |
+
{
|
| 713 |
+
"id": "C8X-نافذة-wal",
|
| 714 |
+
"category": 8,
|
| 715 |
+
"input": "والنافذة",
|
| 716 |
+
"root": "نافذة",
|
| 717 |
+
"prefix_combo": "wal",
|
| 718 |
+
"track_a_spelling": "النافذة",
|
| 719 |
+
"changed": true
|
| 720 |
+
},
|
| 721 |
+
{
|
| 722 |
+
"id": "C8X-نافذة-bal",
|
| 723 |
+
"category": 8,
|
| 724 |
+
"input": "بالنافذة",
|
| 725 |
+
"root": "نافذة",
|
| 726 |
+
"prefix_combo": "bal",
|
| 727 |
+
"track_a_spelling": "النافذة",
|
| 728 |
+
"changed": true
|
| 729 |
+
},
|
| 730 |
+
{
|
| 731 |
+
"id": "C8X-نافذة-lal",
|
| 732 |
+
"category": 8,
|
| 733 |
+
"input": "للنافذة",
|
| 734 |
+
"root": "نافذة",
|
| 735 |
+
"prefix_combo": "lal",
|
| 736 |
+
"track_a_spelling": "النافذة",
|
| 737 |
+
"changed": true
|
| 738 |
+
},
|
| 739 |
+
{
|
| 740 |
+
"id": "C8X-علم-al",
|
| 741 |
+
"category": 8,
|
| 742 |
+
"input": "العلم",
|
| 743 |
+
"root": "علم",
|
| 744 |
+
"prefix_combo": "al",
|
| 745 |
+
"track_a_spelling": "العلم",
|
| 746 |
+
"changed": false
|
| 747 |
+
},
|
| 748 |
+
{
|
| 749 |
+
"id": "C8X-علم-wal",
|
| 750 |
+
"category": 8,
|
| 751 |
+
"input": "والعلم",
|
| 752 |
+
"root": "علم",
|
| 753 |
+
"prefix_combo": "wal",
|
| 754 |
+
"track_a_spelling": "والعلم هو العلم",
|
| 755 |
+
"changed": true
|
| 756 |
+
},
|
| 757 |
+
{
|
| 758 |
+
"id": "C8X-علم-bal",
|
| 759 |
+
"category": 8,
|
| 760 |
+
"input": "بالعلم",
|
| 761 |
+
"root": "علم",
|
| 762 |
+
"prefix_combo": "bal",
|
| 763 |
+
"track_a_spelling": "العلم بالعلم",
|
| 764 |
+
"changed": true
|
| 765 |
+
},
|
| 766 |
+
{
|
| 767 |
+
"id": "C8X-علم-lal",
|
| 768 |
+
"category": 8,
|
| 769 |
+
"input": "للعلم",
|
| 770 |
+
"root": "علم",
|
| 771 |
+
"prefix_combo": "lal",
|
| 772 |
+
"track_a_spelling": "للعلم",
|
| 773 |
+
"changed": false
|
| 774 |
+
},
|
| 775 |
+
{
|
| 776 |
+
"id": "C8X-اقتصاد-al",
|
| 777 |
+
"category": 8,
|
| 778 |
+
"input": "الاقتصاد",
|
| 779 |
+
"root": "اقتصاد",
|
| 780 |
+
"prefix_combo": "al",
|
| 781 |
+
"track_a_spelling": "الاقتصاد",
|
| 782 |
+
"changed": false
|
| 783 |
+
},
|
| 784 |
+
{
|
| 785 |
+
"id": "C8X-اقتصاد-wal",
|
| 786 |
+
"category": 8,
|
| 787 |
+
"input": "والاقتصاد",
|
| 788 |
+
"root": "اقتصاد",
|
| 789 |
+
"prefix_combo": "wal",
|
| 790 |
+
"track_a_spelling": "والاقتصاد",
|
| 791 |
+
"changed": false
|
| 792 |
+
},
|
| 793 |
+
{
|
| 794 |
+
"id": "C8X-اقتصاد-bal",
|
| 795 |
+
"category": 8,
|
| 796 |
+
"input": "بالاقتصاد",
|
| 797 |
+
"root": "اقتصاد",
|
| 798 |
+
"prefix_combo": "bal",
|
| 799 |
+
"track_a_spelling": "بالاقتصاد في الاقتصاد",
|
| 800 |
+
"changed": true
|
| 801 |
+
},
|
| 802 |
+
{
|
| 803 |
+
"id": "C8X-اقتصاد-lal",
|
| 804 |
+
"category": 8,
|
| 805 |
+
"input": "للاقتصاد",
|
| 806 |
+
"root": "اقتصاد",
|
| 807 |
+
"prefix_combo": "lal",
|
| 808 |
+
"track_a_spelling": "للاقتصاد الاقتصادي",
|
| 809 |
+
"changed": true
|
| 810 |
+
}
|
| 811 |
+
],
|
| 812 |
+
"cat9x": [
|
| 813 |
+
{
|
| 814 |
+
"id": "C9X-01",
|
| 815 |
+
"category": 9,
|
| 816 |
+
"input": "إنّ",
|
| 817 |
+
"context": "isolation",
|
| 818 |
+
"concern": "stays إنّ",
|
| 819 |
+
"track_a_spelling": "إن إن",
|
| 820 |
+
"changed": true
|
| 821 |
+
},
|
| 822 |
+
{
|
| 823 |
+
"id": "C9X-02",
|
| 824 |
+
"category": 9,
|
| 825 |
+
"input": "أنّ",
|
| 826 |
+
"context": "isolation",
|
| 827 |
+
"concern": "stays أنّ",
|
| 828 |
+
"track_a_spelling": "أن أن",
|
| 829 |
+
"changed": true
|
| 830 |
+
},
|
| 831 |
+
{
|
| 832 |
+
"id": "C9X-03",
|
| 833 |
+
"category": 9,
|
| 834 |
+
"input": "إنّ العلم نور",
|
| 835 |
+
"context": "sentence",
|
| 836 |
+
"concern": "إنّ stays",
|
| 837 |
+
"track_a_spelling": "إن العلم نور",
|
| 838 |
+
"changed": true
|
| 839 |
+
},
|
| 840 |
+
{
|
| 841 |
+
"id": "C9X-04",
|
| 842 |
+
"category": 9,
|
| 843 |
+
"input": "علمت أنّ الامتحان صعب",
|
| 844 |
+
"context": "sentence",
|
| 845 |
+
"concern": "أنّ stays",
|
| 846 |
+
"track_a_spelling": "علمت أن الامتحان صعب",
|
| 847 |
+
"changed": true
|
| 848 |
+
},
|
| 849 |
+
{
|
| 850 |
+
"id": "C9X-05",
|
| 851 |
+
"category": 9,
|
| 852 |
+
"input": "علي",
|
| 853 |
+
"context": "isolation",
|
| 854 |
+
"concern": "could be name علي or على",
|
| 855 |
+
"track_a_spelling": "علي",
|
| 856 |
+
"changed": false
|
| 857 |
+
},
|
| 858 |
+
{
|
| 859 |
+
"id": "C9X-06",
|
| 860 |
+
"category": 9,
|
| 861 |
+
"input": "ذهب علي إلى المدرسة",
|
| 862 |
+
"context": "sentence",
|
| 863 |
+
"concern": "علي is a name here",
|
| 864 |
+
"track_a_spelling": "ذهب علي إلى المدرسة",
|
| 865 |
+
"changed": false
|
| 866 |
+
},
|
| 867 |
+
{
|
| 868 |
+
"id": "C9X-07",
|
| 869 |
+
"category": 9,
|
| 870 |
+
"input": "جلس علي الكرسي",
|
| 871 |
+
"context": "sentence",
|
| 872 |
+
"concern": "AMBIGUOUS: علي=name or على=on",
|
| 873 |
+
"track_a_spelling": "جلس علي الكرسي",
|
| 874 |
+
"changed": false
|
| 875 |
+
}
|
| 876 |
+
],
|
| 877 |
+
"cat10x": [
|
| 878 |
+
{
|
| 879 |
+
"id": "C10X-01a",
|
| 880 |
+
"category": 10,
|
| 881 |
+
"input": "الحديقه جميلة جدا",
|
| 882 |
+
"concern": "error_at_start",
|
| 883 |
+
"track_a_spelling": "الحديقه جميلة جدا",
|
| 884 |
+
"a_changed": false,
|
| 885 |
+
"track_b_corrected": "الحديقة جميلة جدا.",
|
| 886 |
+
"track_b_suggestions": 2
|
| 887 |
+
},
|
| 888 |
+
{
|
| 889 |
+
"id": "C10X-01b",
|
| 890 |
+
"category": 10,
|
| 891 |
+
"input": "الجو حار في الحديقه",
|
| 892 |
+
"concern": "error_at_end",
|
| 893 |
+
"track_a_spelling": "الجو حار في الحديقة",
|
| 894 |
+
"a_changed": true,
|
| 895 |
+
"track_b_corrected": "الجو حار في الحديقة.",
|
| 896 |
+
"track_b_suggestions": 1
|
| 897 |
+
},
|
| 898 |
+
{
|
| 899 |
+
"id": "C10X-02a",
|
| 900 |
+
"category": 10,
|
| 901 |
+
"input": "الى المدرسة ذهب الولد",
|
| 902 |
+
"concern": "error_at_start",
|
| 903 |
+
"track_a_spelling": "إلى المدرسة ذهب الولد",
|
| 904 |
+
"a_changed": true,
|
| 905 |
+
"track_b_corrected": "إلى المدرسة ذهب الولد.",
|
| 906 |
+
"track_b_suggestions": 2
|
| 907 |
+
},
|
| 908 |
+
{
|
| 909 |
+
"id": "C10X-02b",
|
| 910 |
+
"category": 10,
|
| 911 |
+
"input": "ذهب الولد الى المدرسة",
|
| 912 |
+
"concern": "error_at_end",
|
| 913 |
+
"track_a_spelling": "ذهب الولد إلى المدرسة",
|
| 914 |
+
"a_changed": true,
|
| 915 |
+
"track_b_corrected": "ذهب الولد إلى المدرسة.",
|
| 916 |
+
"track_b_suggestions": 2
|
| 917 |
+
},
|
| 918 |
+
{
|
| 919 |
+
"id": "C10X-DRIFT",
|
| 920 |
+
"category": 10,
|
| 921 |
+
"input_len": 713,
|
| 922 |
+
"word_count": 119,
|
| 923 |
+
"total_suggestions": 16,
|
| 924 |
+
"front_half_suggestions": 11,
|
| 925 |
+
"back_half_suggestions": 5,
|
| 926 |
+
"coordinate_mismatches": [],
|
| 927 |
+
"suggestions_detail": [
|
| 928 |
+
{
|
| 929 |
+
"alternatives": [],
|
| 930 |
+
"confidence": 1.0,
|
| 931 |
+
"correction": "إحدىهن وبدأت",
|
| 932 |
+
"end": 62,
|
| 933 |
+
"id": "e892df95-0d05-40bd-969a-ccda1305cf2c",
|
| 934 |
+
"locked": true,
|
| 935 |
+
"original": "احداهن وبدءت",
|
| 936 |
+
"priority": 3,
|
| 937 |
+
"start": 50,
|
| 938 |
+
"type": "grammar"
|
| 939 |
+
},
|
| 940 |
+
{
|
| 941 |
+
"alternatives": [],
|
| 942 |
+
"confidence": 1.0,
|
| 943 |
+
"correction": "اجتهدوا",
|
| 944 |
+
"end": 243,
|
| 945 |
+
"id": "eef5aad7-31f7-4c1e-8095-88dbdda98944",
|
| 946 |
+
"locked": true,
|
| 947 |
+
"original": "اجتهدو",
|
| 948 |
+
"priority": 3,
|
| 949 |
+
"start": 237,
|
| 950 |
+
"type": "grammar"
|
| 951 |
+
},
|
| 952 |
+
{
|
| 953 |
+
"alternatives": [],
|
| 954 |
+
"confidence": 1.0,
|
| 955 |
+
"correction": "حققوا",
|
| 956 |
+
"end": 259,
|
| 957 |
+
"id": "abfaa89c-119e-4899-9456-6ee78c929298",
|
| 958 |
+
"locked": true,
|
| 959 |
+
"original": "حققو",
|
| 960 |
+
"priority": 3,
|
| 961 |
+
"start": 255,
|
| 962 |
+
"type": "grammar"
|
| 963 |
+
},
|
| 964 |
+
{
|
| 965 |
+
"alternatives": [],
|
| 966 |
+
"confidence": 1.0,
|
| 967 |
+
"correction": "",
|
| 968 |
+
"end": 712,
|
| 969 |
+
"id": "afcc69ca-f5d8-4907-b85a-e348d0d06a12",
|
| 970 |
+
"locked": true,
|
| 971 |
+
"original": "بين الأشجار",
|
| 972 |
+
"priority": 3,
|
| 973 |
+
"start": 701,
|
| 974 |
+
"type": "grammar"
|
| 975 |
+
},
|
| 976 |
+
{
|
| 977 |
+
"alternatives": [],
|
| 978 |
+
"confidence": 0.8,
|
| 979 |
+
"correction": "محمد،",
|
| 980 |
+
"end": 282,
|
| 981 |
+
"id": "4eef8996-7a31-4d0e-83ca-e05604b975e0",
|
| 982 |
+
"locked": true,
|
| 983 |
+
"original": "محمد",
|
| 984 |
+
"priority": 2,
|
| 985 |
+
"start": 278,
|
| 986 |
+
"type": "punctuation"
|
| 987 |
+
},
|
| 988 |
+
{
|
| 989 |
+
"alternatives": [],
|
| 990 |
+
"confidence": 0.8,
|
| 991 |
+
"correction": "جمهورية،",
|
| 992 |
+
"end": 424,
|
| 993 |
+
"id": "7e82e486-59af-4002-be9b-5b202dfe8492",
|
| 994 |
+
"locked": true,
|
| 995 |
+
"original": "جمهورية",
|
| 996 |
+
"priority": 2,
|
| 997 |
+
"start": 417,
|
| 998 |
+
"type": "punctuation"
|
| 999 |
+
},
|
| 1000 |
+
{
|
| 1001 |
+
"alternatives": [],
|
| 1002 |
+
"confidence": 0.8,
|
| 1003 |
+
"correction": "بين،",
|
| 1004 |
+
"end": 497,
|
| 1005 |
+
"id": "6b95fcb5-e190-4dec-8d69-22520c1bb6fe",
|
| 1006 |
+
"locked": true,
|
| 1007 |
+
"original": "بين",
|
| 1008 |
+
"priority": 2,
|
| 1009 |
+
"start": 494,
|
| 1010 |
+
"type": "punctuation"
|
| 1011 |
+
},
|
| 1012 |
+
{
|
| 1013 |
+
"alternatives": [],
|
| 1014 |
+
"confidence": 1.0,
|
| 1015 |
+
"correction": "الحديقة الجميلة وفجأة",
|
| 1016 |
+
"end": 44,
|
| 1017 |
+
"id": "73493796-1711-4996-9ee4-7013191bc9d8",
|
| 1018 |
+
"locked": true,
|
| 1019 |
+
"original": "الحديقه الجميله وفجأه",
|
| 1020 |
+
"priority": 1,
|
| 1021 |
+
"start": 23,
|
| 1022 |
+
"type": "spelling"
|
| 1023 |
+
},
|
| 1024 |
+
{
|
| 1025 |
+
"alternatives": [],
|
| 1026 |
+
"confidence": 1.0,
|
| 1027 |
+
"correction": "بشدة",
|
| 1028 |
+
"end": 72,
|
| 1029 |
+
"id": "13c914ea-5b75-4128-aa42-05576b3d55ae",
|
| 1030 |
+
"locked": true,
|
| 1031 |
+
"original": "بشده",
|
| 1032 |
+
"priority": 1,
|
| 1033 |
+
"start": 68,
|
| 1034 |
+
"type": "spelling"
|
| 1035 |
+
},
|
| 1036 |
+
{
|
| 1037 |
+
"alternatives": [],
|
| 1038 |
+
"confidence": 1.0,
|
| 1039 |
+
"correction": "إلى المدرسة",
|
| 1040 |
+
"end": 94,
|
| 1041 |
+
"id": "4e827496-d6f1-4a53-b2f7-c78c2d911195",
|
| 1042 |
+
"locked": true,
|
| 1043 |
+
"original": "الى المدرسه",
|
| 1044 |
+
"priority": 1,
|
| 1045 |
+
"start": 83,
|
| 1046 |
+
"type": "spelling"
|
| 1047 |
+
},
|
| 1048 |
+
{
|
| 1049 |
+
"alternatives": [],
|
| 1050 |
+
"confidence": 1.0,
|
| 1051 |
+
"correction": "المعلمة وأخذ",
|
| 1052 |
+
"end": 113,
|
| 1053 |
+
"id": "d7548b59-6379-4b95-a5ed-806e5d1d0cfb",
|
| 1054 |
+
"locked": true,
|
| 1055 |
+
"original": "المعلمه واخذ",
|
| 1056 |
+
"priority": 1,
|
| 1057 |
+
"start": 101,
|
| 1058 |
+
"type": "spelling"
|
| 1059 |
+
},
|
| 1060 |
+
{
|
| 1061 |
+
"alternatives": [],
|
| 1062 |
+
"confidence": 1.0,
|
| 1063 |
+
"correction": "أن",
|
| 1064 |
+
"end": 123,
|
| 1065 |
+
"id": "aa9b0140-5740-4343-a3a2-3adfa61fa9d9",
|
| 1066 |
+
"locked": true,
|
| 1067 |
+
"original": "ان",
|
| 1068 |
+
"priority": 1,
|
| 1069 |
+
"start": 121,
|
| 1070 |
+
"type": "spelling"
|
| 1071 |
+
},
|
| 1072 |
+
{
|
| 1073 |
+
"alternatives": [],
|
| 1074 |
+
"confidence": 1.0,
|
| 1075 |
+
"correction": "هذه المدينة جميلة",
|
| 1076 |
+
"end": 194,
|
| 1077 |
+
"id": "03378376-a164-46c8-8493-55a0dcd97e3e",
|
| 1078 |
+
"locked": true,
|
| 1079 |
+
"original": "هذة المدينه جميله",
|
| 1080 |
+
"priority": 1,
|
| 1081 |
+
"start": 177,
|
| 1082 |
+
"type": "spelling"
|
| 1083 |
+
},
|
| 1084 |
+
{
|
| 1085 |
+
"alternatives": [],
|
| 1086 |
+
"confidence": 1.0,
|
| 1087 |
+
"correction": "ممتازة",
|
| 1088 |
+
"end": 272,
|
| 1089 |
+
"id": "b3aac62f-6a7c-4625-b608-5258fea91fcd",
|
| 1090 |
+
"locked": true,
|
| 1091 |
+
"original": "ممتازه",
|
| 1092 |
+
"priority": 1,
|
| 1093 |
+
"start": 266,
|
| 1094 |
+
"type": "spelling"
|
| 1095 |
+
},
|
| 1096 |
+
{
|
| 1097 |
+
"alternatives": [],
|
| 1098 |
+
"confidence": 1.0,
|
| 1099 |
+
"correction": "هذه المحاضرة",
|
| 1100 |
+
"end": 632,
|
| 1101 |
+
"id": "e7994f0f-dc27-4c01-b055-0040683a7643",
|
| 1102 |
+
"locked": true,
|
| 1103 |
+
"original": "هذة المحاضره",
|
| 1104 |
+
"priority": 1,
|
| 1105 |
+
"start": 620,
|
| 1106 |
+
"type": "spelling"
|
| 1107 |
+
},
|
| 1108 |
+
{
|
| 1109 |
+
"alternatives": [],
|
| 1110 |
+
"confidence": 1.0,
|
| 1111 |
+
"correction": "أهمية",
|
| 1112 |
+
"end": 641,
|
| 1113 |
+
"id": "f68bfac9-17f8-4bbe-9def-1ee35e6ac76a",
|
| 1114 |
+
"locked": true,
|
| 1115 |
+
"original": "اهمية",
|
| 1116 |
+
"priority": 1,
|
| 1117 |
+
"start": 636,
|
| 1118 |
+
"type": "spelling"
|
| 1119 |
+
}
|
| 1120 |
+
]
|
| 1121 |
+
}
|
| 1122 |
+
],
|
| 1123 |
+
"cat11": [
|
| 1124 |
+
{
|
| 1125 |
+
"id": "C11-01",
|
| 1126 |
+
"category": 11,
|
| 1127 |
+
"input": "",
|
| 1128 |
+
"desc": "empty_string",
|
| 1129 |
+
"input_len": 0,
|
| 1130 |
+
"crashed": false,
|
| 1131 |
+
"b_corrected": "",
|
| 1132 |
+
"b_suggestions": 0,
|
| 1133 |
+
"error": "HTTP 400: {\"error\":\"Text is required\",\"status\":\"error\"}\n"
|
| 1134 |
+
},
|
| 1135 |
+
{
|
| 1136 |
+
"id": "C11-02",
|
| 1137 |
+
"category": 11,
|
| 1138 |
+
"input": " ",
|
| 1139 |
+
"desc": "whitespace_only",
|
| 1140 |
+
"input_len": 1,
|
| 1141 |
+
"crashed": false,
|
| 1142 |
+
"b_corrected": " ",
|
| 1143 |
+
"b_suggestions": 0,
|
| 1144 |
+
"error": "HTTP 400: {\"error\":\"Text is required\",\"status\":\"error\"}\n"
|
| 1145 |
+
},
|
| 1146 |
+
{
|
| 1147 |
+
"id": "C11-03",
|
| 1148 |
+
"category": 11,
|
| 1149 |
+
"input": "أ",
|
| 1150 |
+
"desc": "single_char",
|
| 1151 |
+
"input_len": 1,
|
| 1152 |
+
"crashed": false,
|
| 1153 |
+
"b_corrected": "أ؟",
|
| 1154 |
+
"b_suggestions": 1,
|
| 1155 |
+
"error": null
|
| 1156 |
+
},
|
| 1157 |
+
{
|
| 1158 |
+
"id": "C11-04",
|
| 1159 |
+
"category": 11,
|
| 1160 |
+
"input": "مستشفياتهم",
|
| 1161 |
+
"desc": "long_single_word",
|
| 1162 |
+
"input_len": 10,
|
| 1163 |
+
"crashed": false,
|
| 1164 |
+
"b_corrected": "في مستشفيات هم",
|
| 1165 |
+
"b_suggestions": 1,
|
| 1166 |
+
"error": null
|
| 1167 |
+
},
|
| 1168 |
+
{
|
| 1169 |
+
"id": "C11-05",
|
| 1170 |
+
"category": 11,
|
| 1171 |
+
"input": "ذهبالولدالىالمدرسةوقابلالمعلمة",
|
| 1172 |
+
"desc": "no_spaces",
|
| 1173 |
+
"input_len": 30,
|
| 1174 |
+
"crashed": false,
|
| 1175 |
+
"b_corrected": "ذهبالولدالىالمدرسةوقابلالمعلمة.",
|
| 1176 |
+
"b_suggestions": 1,
|
| 1177 |
+
"error": null
|
| 1178 |
+
},
|
| 1179 |
+
{
|
| 1180 |
+
"id": "C11-06",
|
| 1181 |
+
"category": 11,
|
| 1182 |
+
"input": "...!؟،،؛؛::...",
|
| 1183 |
+
"desc": "all_punctuation",
|
| 1184 |
+
"input_len": 14,
|
| 1185 |
+
"crashed": false,
|
| 1186 |
+
"b_corrected": ". ! ؟ ، ؛ ::.",
|
| 1187 |
+
"b_suggestions": 1,
|
| 1188 |
+
"error": null
|
| 1189 |
+
},
|
| 1190 |
+
{
|
| 1191 |
+
"id": "C11-07",
|
| 1192 |
+
"category": 11,
|
| 1193 |
+
"input": "(([{هذا النص}]))",
|
| 1194 |
+
"desc": "unbalanced_brackets",
|
| 1195 |
+
"input_len": 16,
|
| 1196 |
+
"crashed": false,
|
| 1197 |
+
"b_corrected": "( ( [ { هذا النص } ] ، و",
|
| 1198 |
+
"b_suggestions": 1,
|
| 1199 |
+
"error": null
|
| 1200 |
+
},
|
| 1201 |
+
{
|
| 1202 |
+
"id": "C11-08",
|
| 1203 |
+
"category": 11,
|
| 1204 |
+
"input": "\"هذا\" 'نص' «اختبار»",
|
| 1205 |
+
"desc": "mixed_quotes",
|
| 1206 |
+
"input_len": 19,
|
| 1207 |
+
"crashed": false,
|
| 1208 |
+
"b_corrected": "\" هذا \" مُنصا ' ' « اختبارا »",
|
| 1209 |
+
"b_suggestions": 1,
|
| 1210 |
+
"error": null
|
| 1211 |
+
},
|
| 1212 |
+
{
|
| 1213 |
+
"id": "C11-09",
|
| 1214 |
+
"category": 11,
|
| 1215 |
+
"input": "يستخدم الذكاء الاصطناعي تقنيات التعلم العميق يستخدم الذكاء الاصطناعي تقنيات التعلم العميق يستخدم الذكاء الاصطناعي تقنيات التعلم العميق يستخدم الذكاء الاصطناعي تقنيات التعلم العميق يستخدم الذكاء الاصطن",
|
| 1216 |
+
"desc": "len_299",
|
| 1217 |
+
"input_len": 299,
|
| 1218 |
+
"crashed": false,
|
| 1219 |
+
"b_corrected": "يستخدم الذكاء الاصطناعي تقنيات، التعلم العميق تستخدم الذكاء الاالعميق،ناعي التقنيات التالتعلم،م العميق يستخدم الذكاء الاصطناعية تقنيات التعلم العميقة يستخدم الذكاء الصناعي تقنيات التعلم عميقا يستخدم ا",
|
| 1220 |
+
"b_suggestions": 7,
|
| 1221 |
+
"error": null
|
| 1222 |
+
},
|
| 1223 |
+
{
|
| 1224 |
+
"id": "C11-10",
|
| 1225 |
+
"category": 11,
|
| 1226 |
+
"input": "يستخدم الذكاء الاصطناعي تقنيات التعلم العميق يستخدم الذكاء الاصطناعي تقنيات التعلم العميق يستخدم الذكاء الاصطناعي تقنيات التعلم العميق يستخدم الذكاء الاصطناعي تقنيات التعلم العميق يستخدم الذكاء الاصطن",
|
| 1227 |
+
"desc": "len_300",
|
| 1228 |
+
"input_len": 300,
|
| 1229 |
+
"crashed": false,
|
| 1230 |
+
"b_corrected": "يستخدم الذكاء الاصطناعي تقنيات، التعلم العميق تستخدم الذكاء االعميق،صطنالذكاء، التقنيات التعلم العميق يستخدم الذكاء الاصطناعية تقنيات التعلم العميقة يستخدم الذكاء الصناعي تقنيات التعلم عميقا يستخدم ال",
|
| 1231 |
+
"b_suggestions": 14,
|
| 1232 |
+
"error": null
|
| 1233 |
+
},
|
| 1234 |
+
{
|
| 1235 |
+
"id": "C11-11",
|
| 1236 |
+
"category": 11,
|
| 1237 |
+
"input": "يستخدم الذكاء الاصطناعي تقنيات التعلم العميق يستخدم الذكاء الاصطناعي تقنيات التعلم العميق يستخدم الذكاء الاصطناعي تقنيات التعلم العميق يستخدم الذكاء الاصطناعي تقنيات التعلم العميق يستخدم الذكاء الاصطن",
|
| 1238 |
+
"desc": "len_301",
|
| 1239 |
+
"input_len": 301,
|
| 1240 |
+
"crashed": false,
|
| 1241 |
+
"b_corrected": "يستخدم الذكاء الاصطناعي تقنيات، التعلم العميق تستخدم الذكاء االعميق،صطنالذكاء، التقنيات التعلم العميق يستخدم الذكاء الاصطناعية تقنيات التعلم العميقة يستخدم الذكاء الصناعي تقنيات التعلم عميقا يستخدم ال",
|
| 1242 |
+
"b_suggestions": 14,
|
| 1243 |
+
"error": null
|
| 1244 |
+
},
|
| 1245 |
+
{
|
| 1246 |
+
"id": "C11-12",
|
| 1247 |
+
"category": 11,
|
| 1248 |
+
"input": "يلعب الطلاب في الحديقه بعد المدرسه وقبل العشاء",
|
| 1249 |
+
"desc": "multi_stage_disagreement",
|
| 1250 |
+
"input_len": 46,
|
| 1251 |
+
"crashed": false,
|
| 1252 |
+
"b_corrected": "يلعب الطلاب في الحديقة بعد المدرسة وقبل العشاء.",
|
| 1253 |
+
"b_suggestions": 3,
|
| 1254 |
+
"error": null
|
| 1255 |
+
},
|
| 1256 |
+
{
|
| 1257 |
+
"id": "C11-13",
|
| 1258 |
+
"category": 11,
|
| 1259 |
+
"input": "الحمد لله",
|
| 1260 |
+
"desc": "model_returns_identical",
|
| 1261 |
+
"input_len": 9,
|
| 1262 |
+
"crashed": false,
|
| 1263 |
+
"b_corrected": "الحمد لله.",
|
| 1264 |
+
"b_suggestions": 1,
|
| 1265 |
+
"error": null
|
| 1266 |
+
},
|
| 1267 |
+
{
|
| 1268 |
+
"id": "C11-14",
|
| 1269 |
+
"category": 11,
|
| 1270 |
+
"input": "مرحبا مرحبا مرحبا مرحبا مرحبا مرحبا مرحبا مرحبا مرحبا مرحبا مرحبا مرحبا مرحبا مرحبا مرحبا مرحبا مرحبا مرحبا مرحبا مرحبا مرحبا مرحبا مرحبا مرحبا مرحبا مرحبا مرحبا مرحبا مرحبا مرحبا مرحبا مرحبا مرحبا مر",
|
| 1271 |
+
"desc": "100x_repeated_word",
|
| 1272 |
+
"input_len": 600,
|
| 1273 |
+
"crashed": false,
|
| 1274 |
+
"b_corrected": "مرحبا مرحبا مرحبا ومرحبا مرحبا مرحبامرحبا مرحبا مرحبا مرحب مرحبا مرحبا وسهلا مرحبا مرحبا ترحيبا مرحبا مرحبا يا مرحبا مرحبا نرحب مرحبا مرحبا ترحيب مرحبا مرحبا أهلا مرحبا مرحبا اهلا مرحبا مرحبا وداعا مر",
|
| 1275 |
+
"b_suggestions": 4,
|
| 1276 |
+
"error": null
|
| 1277 |
+
},
|
| 1278 |
+
{
|
| 1279 |
+
"id": "C11-15",
|
| 1280 |
+
"category": 11,
|
| 1281 |
+
"input": "I went to the مدرسة and met the معلم in the فصل",
|
| 1282 |
+
"desc": "heavy_code_switch",
|
| 1283 |
+
"input_len": 47,
|
| 1284 |
+
"crashed": false,
|
| 1285 |
+
"b_corrected": "I went to the مدرسة and met the معلم in the الفصل.",
|
| 1286 |
+
"b_suggestions": 1,
|
| 1287 |
+
"error": null
|
| 1288 |
+
},
|
| 1289 |
+
{
|
| 1290 |
+
"id": "C11-16",
|
| 1291 |
+
"category": 11,
|
| 1292 |
+
"input": "ايش هالحكي يا زلمة",
|
| 1293 |
+
"desc": "levantine_dialect",
|
| 1294 |
+
"input_len": 18,
|
| 1295 |
+
"crashed": false,
|
| 1296 |
+
"b_corrected": "إيش هالحكي يا زلمة؟",
|
| 1297 |
+
"b_suggestions": 2,
|
| 1298 |
+
"error": null
|
| 1299 |
+
},
|
| 1300 |
+
{
|
| 1301 |
+
"id": "C11-17",
|
| 1302 |
+
"category": 11,
|
| 1303 |
+
"input": "شنو تسوي هسه",
|
| 1304 |
+
"desc": "iraqi_dialect",
|
| 1305 |
+
"input_len": 12,
|
| 1306 |
+
"crashed": false,
|
| 1307 |
+
"b_corrected": "شنو تسوي هسة",
|
| 1308 |
+
"b_suggestions": 1,
|
| 1309 |
+
"error": null
|
| 1310 |
+
},
|
| 1311 |
+
{
|
| 1312 |
+
"id": "C11-RACE",
|
| 1313 |
+
"category": 11,
|
| 1314 |
+
"input": "كانت الفتيات يلعبون في الحديقه",
|
| 1315 |
+
"desc": "parallel_race_condition",
|
| 1316 |
+
"r1_corrected": "كانت الفتيات يلعبن في الحديقة.",
|
| 1317 |
+
"r2_corrected": "كانت الفتيات يلعبن في الحديقة.",
|
| 1318 |
+
"r1_suggestions": 2,
|
| 1319 |
+
"r2_suggestions": 2,
|
| 1320 |
+
"identical": true
|
| 1321 |
+
}
|
| 1322 |
+
]
|
| 1323 |
+
}
|
archive/old_tests/deep_dive_expanded.py
ADDED
|
@@ -0,0 +1,428 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
BAYAN Deep-Dive Test Harness — EXPANDED (ALL Categories)
|
| 3 |
+
Covers every item from the original prompt that was missing.
|
| 4 |
+
"""
|
| 5 |
+
import sys, os, re, json, time, argparse, concurrent.futures
|
| 6 |
+
from datetime import datetime, timezone
|
| 7 |
+
import requests
|
| 8 |
+
|
| 9 |
+
API_BASE = "https://bayan10-bayan-api.hf.space"
|
| 10 |
+
TIMEOUT = 60
|
| 11 |
+
|
| 12 |
+
def api_call(endpoint, text, retries=2):
|
| 13 |
+
url = f"{API_BASE}{endpoint}"
|
| 14 |
+
for attempt in range(retries + 1):
|
| 15 |
+
try:
|
| 16 |
+
t0 = time.time()
|
| 17 |
+
resp = requests.post(url, json={"text": text}, timeout=TIMEOUT)
|
| 18 |
+
elapsed = int((time.time() - t0) * 1000)
|
| 19 |
+
if resp.status_code == 200:
|
| 20 |
+
data = resp.json()
|
| 21 |
+
data['_elapsed_ms'] = elapsed
|
| 22 |
+
data['_timestamp'] = datetime.now(timezone.utc).isoformat()
|
| 23 |
+
return data
|
| 24 |
+
else:
|
| 25 |
+
if attempt < retries:
|
| 26 |
+
time.sleep(2)
|
| 27 |
+
continue
|
| 28 |
+
return {"error": f"HTTP {resp.status_code}: {resp.text[:200]}", "_elapsed_ms": elapsed}
|
| 29 |
+
except requests.exceptions.Timeout:
|
| 30 |
+
return {"error": f"Timeout after {TIMEOUT}s"}
|
| 31 |
+
except Exception as e:
|
| 32 |
+
return {"error": str(e)}
|
| 33 |
+
|
| 34 |
+
def track_a_spelling(text):
|
| 35 |
+
r = api_call("/api/spelling", text)
|
| 36 |
+
if "error" in r and "corrected_text" not in r:
|
| 37 |
+
return {"input": text, "output": text, "error": r["error"], "changed": False}
|
| 38 |
+
c = r.get("corrected_text", text)
|
| 39 |
+
return {"input": text, "output": c, "changed": c != text, "elapsed_ms": r.get("_elapsed_ms")}
|
| 40 |
+
|
| 41 |
+
def track_a_grammar(text):
|
| 42 |
+
r = api_call("/api/grammar", text)
|
| 43 |
+
if "error" in r and "corrected_text" not in r:
|
| 44 |
+
return {"input": text, "output": text, "error": r["error"], "changed": False}
|
| 45 |
+
c = r.get("corrected_text", text)
|
| 46 |
+
return {"input": text, "output": c, "changed": c != text, "elapsed_ms": r.get("_elapsed_ms"), "timestamp": r.get("_timestamp")}
|
| 47 |
+
|
| 48 |
+
def track_a_punctuation(text):
|
| 49 |
+
r = api_call("/api/punctuation", text)
|
| 50 |
+
if "error" in r and "corrected_text" not in r:
|
| 51 |
+
return {"input": text, "output": text, "error": r["error"], "changed": False}
|
| 52 |
+
c = r.get("corrected_text", text)
|
| 53 |
+
PUNC = '.,;:!?،؛؟'
|
| 54 |
+
return {"input": text, "output": c, "changed": c != text,
|
| 55 |
+
"marks_added": sum(1 for ch in c if ch in PUNC) - sum(1 for ch in text if ch in PUNC),
|
| 56 |
+
"elapsed_ms": r.get("_elapsed_ms")}
|
| 57 |
+
|
| 58 |
+
def track_b_analyze(text):
|
| 59 |
+
r = api_call("/api/analyze", text)
|
| 60 |
+
if "error" in r and "suggestions" not in r:
|
| 61 |
+
return {"input": text, "error": r["error"], "suggestions": [], "corrected": text}
|
| 62 |
+
return {
|
| 63 |
+
"input": text, "original": r.get("original", text),
|
| 64 |
+
"corrected": r.get("corrected", text),
|
| 65 |
+
"suggestions": r.get("suggestions", []),
|
| 66 |
+
"timing_ms": r.get("timing_ms", {}),
|
| 67 |
+
"elapsed_ms": r.get("_elapsed_ms"),
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
def log(msg):
|
| 71 |
+
print(f"[{datetime.now().strftime('%H:%M:%S')}] {msg}", flush=True)
|
| 72 |
+
|
| 73 |
+
# ═══════════════════════════════════════════════════════════════════
|
| 74 |
+
# CATEGORY 1 — Cross-model mismatch
|
| 75 |
+
# ═══════════════════════════════════════════════════════════════════
|
| 76 |
+
def run_cat1():
|
| 77 |
+
log("=== CATEGORY 1: Cross-model mismatch ===")
|
| 78 |
+
results = []
|
| 79 |
+
inputs = [
|
| 80 |
+
{"id": "C1-01", "input": "كانت الفتيات يلعبون في الحديقه"},
|
| 81 |
+
{"id": "C1-02", "input": "ان الطالبات ذهبو الى الجامعه"},
|
| 82 |
+
{"id": "C1-03", "input": "هذة المدينه جميله جدا ومناخها معتدل"},
|
| 83 |
+
{"id": "C1-04", "input": "الطلاب اجتهدو في دراستهم وحققو نتائج ممتازه"},
|
| 84 |
+
{"id": "C1-05", "input": "ذهب الولد الى المكتبه وقرا كتاب مفيد"},
|
| 85 |
+
]
|
| 86 |
+
for test in inputs:
|
| 87 |
+
log(f" {test['id']}: {test['input'][:50]}...")
|
| 88 |
+
# Track A: each model on ORIGINAL independently
|
| 89 |
+
a_spell = track_a_spelling(test['input'])
|
| 90 |
+
a_gram_on_orig = track_a_grammar(test['input'])
|
| 91 |
+
# NEW: grammar on SPELLING-CORRECTED text
|
| 92 |
+
a_gram_on_spell = track_a_grammar(a_spell['output'])
|
| 93 |
+
a_punc = track_a_punctuation(test['input'])
|
| 94 |
+
# Track B
|
| 95 |
+
b = track_b_analyze(test['input'])
|
| 96 |
+
|
| 97 |
+
# Diff: grammar on original vs grammar on spell-corrected
|
| 98 |
+
gram_orig_words = a_gram_on_orig['output'].split()
|
| 99 |
+
gram_spell_words = a_gram_on_spell['output'].split()
|
| 100 |
+
gram_diff = []
|
| 101 |
+
for i, (w1, w2) in enumerate(zip(gram_orig_words, gram_spell_words)):
|
| 102 |
+
if w1 != w2:
|
| 103 |
+
gram_diff.append({"word_idx": i, "gram_on_orig": w1, "gram_on_spell": w2})
|
| 104 |
+
|
| 105 |
+
result = {
|
| 106 |
+
"id": test['id'], "category": 1, "input": test['input'],
|
| 107 |
+
"a_spelling": a_spell['output'],
|
| 108 |
+
"a_grammar_on_original": a_gram_on_orig['output'],
|
| 109 |
+
"a_grammar_on_spell_corrected": a_gram_on_spell['output'],
|
| 110 |
+
"a_punctuation": a_punc['output'],
|
| 111 |
+
"grammar_diff_orig_vs_spell": gram_diff,
|
| 112 |
+
"b_corrected": b.get('corrected', ''),
|
| 113 |
+
"b_suggestions": b.get('suggestions', []),
|
| 114 |
+
}
|
| 115 |
+
log(f" A_spell: {a_spell['output'][:60]}")
|
| 116 |
+
log(f" A_gram(orig): {a_gram_on_orig['output'][:60]}")
|
| 117 |
+
log(f" A_gram(spell): {a_gram_on_spell['output'][:60]}")
|
| 118 |
+
log(f" Grammar diff: {gram_diff}")
|
| 119 |
+
log(f" B_final: {b.get('corrected','')[:60]}")
|
| 120 |
+
results.append(result)
|
| 121 |
+
return results
|
| 122 |
+
|
| 123 |
+
# ═══════════════════════════════════════════════════════════════════
|
| 124 |
+
# CATEGORY 7 — StageLocker adversarial tests
|
| 125 |
+
# ═══════════════════════════════════════════════════════════════════
|
| 126 |
+
def run_cat7():
|
| 127 |
+
log("=== CATEGORY 7: StageLocker directionality ===")
|
| 128 |
+
results = []
|
| 129 |
+
# 3+ chained mutations: spelling changes length, grammar changes length, punc adds marks
|
| 130 |
+
inputs = [
|
| 131 |
+
{"id": "C7-01", "input": "ذهب الولد الى المدرسه وقابل المعلمه واخذ الكتاب",
|
| 132 |
+
"desc": "3-stage chain: spelling الى→إلى, grammar المدرسه→المدرسة, punc adds marks"},
|
| 133 |
+
{"id": "C7-02", "input": "كانت البنات يلعبون في الحديقه الجميله وفجأه سقطت احداهن",
|
| 134 |
+
"desc": "Multiple overlapping corrections across all stages"},
|
| 135 |
+
{"id": "C7-03", "input": "ان الذكاء الاصطناعي يلعب دورا هاما في تطوير التكنولوجيا الحديثه ولذالك يجب الاهتمام بة",
|
| 136 |
+
"desc": "Long sentence with corrections from all 3 stages"},
|
| 137 |
+
{"id": "C7-04", "input": "هذة المدينه جميله جدا ومناخها معتدل طوال العام وسكانها طيبون جدا",
|
| 138 |
+
"desc": "Multiple ه→ة fixes: does grammar lock prevent punc from adding marks near those words?"},
|
| 139 |
+
{"id": "C7-05", "input": "الطلاب اللذين اجتهدو في دراستهم حققو نتائج ممتازه في الأمتحانات الصعبه",
|
| 140 |
+
"desc": "Heavy corrections needed across stages"},
|
| 141 |
+
]
|
| 142 |
+
for test in inputs:
|
| 143 |
+
log(f" {test['id']}: {test['input'][:50]}...")
|
| 144 |
+
a_spell = track_a_spelling(test['input'])
|
| 145 |
+
a_gram = track_a_grammar(test['input'])
|
| 146 |
+
a_punc = track_a_punctuation(test['input'])
|
| 147 |
+
b = track_b_analyze(test['input'])
|
| 148 |
+
|
| 149 |
+
# Check: are any suggestions at positions that overlap with corrections from earlier stages?
|
| 150 |
+
sugg = b.get('suggestions', [])
|
| 151 |
+
overlaps = []
|
| 152 |
+
for i, s1 in enumerate(sugg):
|
| 153 |
+
for j, s2 in enumerate(sugg):
|
| 154 |
+
if i < j and s1.get('start',0) < s2.get('end',0) and s2.get('start',0) < s1.get('end',0):
|
| 155 |
+
overlaps.append({"s1": s1, "s2": s2})
|
| 156 |
+
|
| 157 |
+
result = {
|
| 158 |
+
"id": test['id'], "category": 7, "input": test['input'],
|
| 159 |
+
"desc": test['desc'],
|
| 160 |
+
"a_spelling": a_spell['output'],
|
| 161 |
+
"a_grammar": a_gram['output'],
|
| 162 |
+
"a_punc": a_punc['output'],
|
| 163 |
+
"b_corrected": b.get('corrected', ''),
|
| 164 |
+
"b_suggestions": sugg,
|
| 165 |
+
"b_suggestion_count": len(sugg),
|
| 166 |
+
"overlapping_suggestions": overlaps,
|
| 167 |
+
}
|
| 168 |
+
log(f" B_final: {b.get('corrected','')[:60]}")
|
| 169 |
+
log(f" Suggestions: {len(sugg)}, Overlaps: {len(overlaps)}")
|
| 170 |
+
results.append(result)
|
| 171 |
+
return results
|
| 172 |
+
|
| 173 |
+
# ═══════════════════════════════════════════════════════════════════
|
| 174 |
+
# CATEGORY 8 EXPANDED — with ال + prefix combos
|
| 175 |
+
# ═══════════════════════════════════════════════════════════════════
|
| 176 |
+
def run_cat8_expanded():
|
| 177 |
+
log("=== CATEGORY 8 EXPANDED: ال + prefix combos ===")
|
| 178 |
+
results = []
|
| 179 |
+
combos = [
|
| 180 |
+
# root, al_form, wal_form, bal_form, lal_form
|
| 181 |
+
("مدرسة", "المدرسة", "والمدرسة", "بالمدرسة", "للمدرسة"),
|
| 182 |
+
("شمس", "الشمس", "والشمس", "بالشمس", "للشمس"),
|
| 183 |
+
("أمة", "الأمة", "والأمة", "بالأمة", "للأمة"),
|
| 184 |
+
("نافذة", "النافذة", "والنافذة", "بالنافذة", "للنافذة"),
|
| 185 |
+
("علم", "العلم", "والعلم", "بالعلم", "للعلم"),
|
| 186 |
+
("اقتصاد", "الاقتصاد", "والاقتصاد", "بالاقتصاد", "للاقتصاد"),
|
| 187 |
+
]
|
| 188 |
+
for root, al, wal, bal, lal in combos:
|
| 189 |
+
for label, word in [("al", al), ("wal", wal), ("bal", bal), ("lal", lal)]:
|
| 190 |
+
a = track_a_spelling(word)
|
| 191 |
+
result = {
|
| 192 |
+
"id": f"C8X-{root}-{label}", "category": 8, "input": word,
|
| 193 |
+
"root": root, "prefix_combo": label,
|
| 194 |
+
"track_a_spelling": a['output'], "changed": a.get('changed', False),
|
| 195 |
+
}
|
| 196 |
+
if a.get('changed'):
|
| 197 |
+
log(f" ⚠ C8X-{root}-{label}: '{word}' → '{a['output']}'")
|
| 198 |
+
results.append(result)
|
| 199 |
+
return results
|
| 200 |
+
|
| 201 |
+
# ═══════════════════════════════════════════════════════════════════
|
| 202 |
+
# CATEGORY 9 EXPANDED — missing pairs
|
| 203 |
+
# ═══════════════════════════════════════════════════════════════════
|
| 204 |
+
def run_cat9_expanded():
|
| 205 |
+
log("=== CATEGORY 9 EXPANDED: Missing confusable pairs ===")
|
| 206 |
+
results = []
|
| 207 |
+
tests = [
|
| 208 |
+
# إنّ / أنّ (with shadda)
|
| 209 |
+
{"id": "C9X-01", "input": "إنّ", "context": "isolation", "concern": "stays إنّ"},
|
| 210 |
+
{"id": "C9X-02", "input": "أنّ", "context": "isolation", "concern": "stays أنّ"},
|
| 211 |
+
{"id": "C9X-03", "input": "إنّ العلم نور", "context": "sentence", "concern": "إنّ stays"},
|
| 212 |
+
{"id": "C9X-04", "input": "علمت أنّ الامتحان صعب", "context": "sentence", "concern": "أنّ stays"},
|
| 213 |
+
# على vs علي (name)
|
| 214 |
+
{"id": "C9X-05", "input": "علي", "context": "isolation", "concern": "could be name علي or على"},
|
| 215 |
+
{"id": "C9X-06", "input": "ذهب علي إلى المدرسة", "context": "sentence", "concern": "علي is a name here"},
|
| 216 |
+
{"id": "C9X-07", "input": "جلس علي الكرسي", "context": "sentence", "concern": "AMBIGUOUS: علي=name or على=on"},
|
| 217 |
+
]
|
| 218 |
+
for test in tests:
|
| 219 |
+
a = track_a_spelling(test['input'])
|
| 220 |
+
result = {
|
| 221 |
+
"id": test['id'], "category": 9, "input": test['input'],
|
| 222 |
+
"context": test['context'], "concern": test['concern'],
|
| 223 |
+
"track_a_spelling": a['output'], "changed": a.get('changed', False),
|
| 224 |
+
}
|
| 225 |
+
if a.get('changed'):
|
| 226 |
+
log(f" ⚠ {test['id']}: '{test['input']}' → '{a['output']}' ({test['concern']})")
|
| 227 |
+
else:
|
| 228 |
+
log(f" ✓ {test['id']}: no change")
|
| 229 |
+
results.append(result)
|
| 230 |
+
return results
|
| 231 |
+
|
| 232 |
+
# ═══════════════════════════════════════════════════════════════════
|
| 233 |
+
# CATEGORY 10 EXPANDED — sentence position + 200-word drift test
|
| 234 |
+
# ═══════════════════════════════════════════════════════════════════
|
| 235 |
+
def run_cat10_expanded():
|
| 236 |
+
log("=== CATEGORY 10 EXPANDED: Position + Cumulative drift ===")
|
| 237 |
+
results = []
|
| 238 |
+
|
| 239 |
+
# Same error at sentence start vs middle
|
| 240 |
+
log(" Sentence-initial vs mid-sentence:")
|
| 241 |
+
position_tests = [
|
| 242 |
+
{"id": "C10X-01a", "input": "الحديقه جميلة جدا", "concern": "error_at_start"},
|
| 243 |
+
{"id": "C10X-01b", "input": "الجو حار في الحديقه", "concern": "error_at_end"},
|
| 244 |
+
{"id": "C10X-02a", "input": "الى المدرسة ذهب الولد", "concern": "error_at_start"},
|
| 245 |
+
{"id": "C10X-02b", "input": "ذهب الولد الى المدرسة", "concern": "error_at_end"},
|
| 246 |
+
]
|
| 247 |
+
for test in position_tests:
|
| 248 |
+
a = track_a_spelling(test['input'])
|
| 249 |
+
b = track_b_analyze(test['input'])
|
| 250 |
+
result = {
|
| 251 |
+
"id": test['id'], "category": 10, "input": test['input'],
|
| 252 |
+
"concern": test['concern'],
|
| 253 |
+
"track_a_spelling": a['output'], "a_changed": a.get('changed', False),
|
| 254 |
+
"track_b_corrected": b.get('corrected', ''),
|
| 255 |
+
"track_b_suggestions": len(b.get('suggestions', [])),
|
| 256 |
+
}
|
| 257 |
+
log(f" {test['id']}: A='{a['output'][:40]}' B_sugg={len(b.get('suggestions',[]))}")
|
| 258 |
+
results.append(result)
|
| 259 |
+
|
| 260 |
+
# 200+ word cumulative drift test
|
| 261 |
+
log("\n 200+ word cumulative drift test:")
|
| 262 |
+
long_text = (
|
| 263 |
+
"كانت الفتيات يلعبون في الحديقه الجميله وفجأه سقطت احداهن وبدءت تبكي بشده "
|
| 264 |
+
"ذهب الولد الى المدرسه وقابل المعلمه واخذ الكتاب "
|
| 265 |
+
"ان الذكاء الاصطناعي يلعب دورا هاما في تطوير التكنولوجيا "
|
| 266 |
+
"هذة المدينه جميله جدا ومناخها معتدل طوال العام "
|
| 267 |
+
"الطلاب الذين اجتهدو في دراستهم حققو نتائج ممتازه "
|
| 268 |
+
"سافر محمد إلى دبي للعمل في شركة جوجل وقابل أصدقاءه القدامى "
|
| 269 |
+
"يستخدم الذكاء الاصطناعي تقنيات التعلم العميق في معالجة البيانات الضخمة "
|
| 270 |
+
"القاهرة عاصمة جمهورية مصر العربية وأكبر مدنها وتقع على ضفاف نهر النيل "
|
| 271 |
+
"تتراوح درجات الحرارة بين خمس وعشرين وثلاثين درجة مئوية في فصل الصيف "
|
| 272 |
+
"الحمد لله رب العالمين الرحمن الرحيم مالك يوم الدين "
|
| 273 |
+
"بسم الله الرحمن الرحيم نبدأ هذة المحاضره عن اهمية التعليم "
|
| 274 |
+
"يحب الأطفال اللعب في الحديقة وركوب الدراجات والجري بين الأشجار "
|
| 275 |
+
)
|
| 276 |
+
word_count = len(long_text.split())
|
| 277 |
+
log(f" Input: {word_count} words, {len(long_text)} chars")
|
| 278 |
+
|
| 279 |
+
b = track_b_analyze(long_text)
|
| 280 |
+
sugg = b.get('suggestions', [])
|
| 281 |
+
# Check coordinates in the back half
|
| 282 |
+
mid_char = len(long_text) // 2
|
| 283 |
+
back_half_sugg = [s for s in sugg if s.get('start', 0) >= mid_char]
|
| 284 |
+
front_half_sugg = [s for s in sugg if s.get('start', 0) < mid_char]
|
| 285 |
+
|
| 286 |
+
# Verify coordinates: does original[start:end] == suggestion['original']?
|
| 287 |
+
coord_mismatches = []
|
| 288 |
+
for s in sugg:
|
| 289 |
+
start, end = s.get('start', 0), s.get('end', 0)
|
| 290 |
+
expected_text = long_text[start:end]
|
| 291 |
+
actual_text = s.get('original', '')
|
| 292 |
+
if expected_text != actual_text:
|
| 293 |
+
coord_mismatches.append({
|
| 294 |
+
"start": start, "end": end,
|
| 295 |
+
"expected_from_coords": expected_text,
|
| 296 |
+
"actual_in_suggestion": actual_text,
|
| 297 |
+
"correction": s.get('correction', ''),
|
| 298 |
+
"type": s.get('type', ''),
|
| 299 |
+
})
|
| 300 |
+
|
| 301 |
+
result = {
|
| 302 |
+
"id": "C10X-DRIFT", "category": 10, "input_len": len(long_text),
|
| 303 |
+
"word_count": word_count,
|
| 304 |
+
"total_suggestions": len(sugg),
|
| 305 |
+
"front_half_suggestions": len(front_half_sugg),
|
| 306 |
+
"back_half_suggestions": len(back_half_sugg),
|
| 307 |
+
"coordinate_mismatches": coord_mismatches,
|
| 308 |
+
"suggestions_detail": sugg,
|
| 309 |
+
}
|
| 310 |
+
log(f" Total suggestions: {len(sugg)} (front: {len(front_half_sugg)}, back: {len(back_half_sugg)})")
|
| 311 |
+
log(f" Coordinate mismatches: {len(coord_mismatches)}")
|
| 312 |
+
for m in coord_mismatches:
|
| 313 |
+
log(f" [{m['start']}:{m['end']}] expected='{m['expected_from_coords']}' got='{m['actual_in_suggestion']}'")
|
| 314 |
+
results.append(result)
|
| 315 |
+
|
| 316 |
+
return results
|
| 317 |
+
|
| 318 |
+
# ═══════════════════════════════════════════════════════════════════
|
| 319 |
+
# CATEGORY 11 — Genuine stress tests / edge cases
|
| 320 |
+
# ═══════════════════════════════════════════════════════════════════
|
| 321 |
+
def run_cat11():
|
| 322 |
+
log("=== CATEGORY 11: Edge case discovery (stress tests) ===")
|
| 323 |
+
results = []
|
| 324 |
+
tests = [
|
| 325 |
+
# Pathological inputs
|
| 326 |
+
{"id": "C11-01", "input": "", "desc": "empty_string"},
|
| 327 |
+
{"id": "C11-02", "input": " ", "desc": "whitespace_only"},
|
| 328 |
+
{"id": "C11-03", "input": "أ", "desc": "single_char"},
|
| 329 |
+
{"id": "C11-04", "input": "مستشفياتهم", "desc": "long_single_word"},
|
| 330 |
+
{"id": "C11-05", "input": "ذهبالولدالىالمدرسةوقابلالمعلمة", "desc": "no_spaces"},
|
| 331 |
+
{"id": "C11-06", "input": "...!؟،،؛؛::...", "desc": "all_punctuation"},
|
| 332 |
+
{"id": "C11-07", "input": "(([{هذا النص}]))", "desc": "unbalanced_brackets"},
|
| 333 |
+
{"id": "C11-08", "input": "\"هذا\" 'نص' «اختبار»", "desc": "mixed_quotes"},
|
| 334 |
+
# Boundary lengths (299, 300, 301 chars)
|
| 335 |
+
{"id": "C11-09", "input": ("يستخدم الذكاء الاصطناعي تقنيات التعلم العميق " * 10)[:299], "desc": "len_299"},
|
| 336 |
+
{"id": "C11-10", "input": ("يستخدم الذكاء الاصطناعي تقنيات التعلم العميق " * 10)[:300], "desc": "len_300"},
|
| 337 |
+
{"id": "C11-11", "input": ("يستخدم الذكاء الاصطناعي تقنيات التعلم العميق " * 10)[:301], "desc": "len_301"},
|
| 338 |
+
# Max disagreement: word that is both plausible spelling error AND grammatically ambiguous
|
| 339 |
+
{"id": "C11-12", "input": "يلعب الطلاب في الحديقه بعد المدرسه وقبل العشاء", "desc": "multi_stage_disagreement"},
|
| 340 |
+
# Correction identical to original (model returns same text)
|
| 341 |
+
{"id": "C11-13", "input": "الحمد لله", "desc": "model_returns_identical"},
|
| 342 |
+
# Very long repetitive text
|
| 343 |
+
{"id": "C11-14", "input": "مرحبا " * 100, "desc": "100x_repeated_word"},
|
| 344 |
+
# Mixed Arabic and English heavily
|
| 345 |
+
{"id": "C11-15", "input": "I went to the مدرسة and met the معلم in the فصل", "desc": "heavy_code_switch"},
|
| 346 |
+
# Dialectal variations
|
| 347 |
+
{"id": "C11-16", "input": "ايش هالحكي يا زلمة", "desc": "levantine_dialect"},
|
| 348 |
+
{"id": "C11-17", "input": "شنو تسوي هسه", "desc": "iraqi_dialect"},
|
| 349 |
+
]
|
| 350 |
+
for test in tests:
|
| 351 |
+
log(f" {test['id']}: '{test['input'][:40]}...' [{test['desc']}]")
|
| 352 |
+
# Track B only for stress tests (we want to see if pipeline crashes)
|
| 353 |
+
b = track_b_analyze(test['input'])
|
| 354 |
+
crashed = "error" in b and "suggestions" not in b
|
| 355 |
+
result = {
|
| 356 |
+
"id": test['id'], "category": 11, "input": test['input'][:200],
|
| 357 |
+
"desc": test['desc'], "input_len": len(test['input']),
|
| 358 |
+
"crashed": crashed,
|
| 359 |
+
"b_corrected": b.get('corrected', '')[:200] if not crashed else "CRASH",
|
| 360 |
+
"b_suggestions": len(b.get('suggestions', [])),
|
| 361 |
+
"error": b.get('error', None),
|
| 362 |
+
}
|
| 363 |
+
status = "💥 CRASH" if crashed else f"✓ ({len(b.get('suggestions',[]))} sugg)"
|
| 364 |
+
log(f" {status}")
|
| 365 |
+
results.append(result)
|
| 366 |
+
|
| 367 |
+
# Race condition: 2 parallel requests with same input
|
| 368 |
+
log("\n Race condition test (2 parallel requests):")
|
| 369 |
+
race_input = "كانت الفتيات يلعبون في الحديقه"
|
| 370 |
+
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as ex:
|
| 371 |
+
f1 = ex.submit(track_b_analyze, race_input)
|
| 372 |
+
f2 = ex.submit(track_b_analyze, race_input)
|
| 373 |
+
r1, r2 = f1.result(), f2.result()
|
| 374 |
+
race_match = r1.get('corrected') == r2.get('corrected') and len(r1.get('suggestions',[])) == len(r2.get('suggestions',[]))
|
| 375 |
+
race_result = {
|
| 376 |
+
"id": "C11-RACE", "category": 11, "input": race_input,
|
| 377 |
+
"desc": "parallel_race_condition",
|
| 378 |
+
"r1_corrected": r1.get('corrected', ''),
|
| 379 |
+
"r2_corrected": r2.get('corrected', ''),
|
| 380 |
+
"r1_suggestions": len(r1.get('suggestions', [])),
|
| 381 |
+
"r2_suggestions": len(r2.get('suggestions', [])),
|
| 382 |
+
"identical": race_match,
|
| 383 |
+
}
|
| 384 |
+
log(f" Race test: identical={race_match}")
|
| 385 |
+
results.append(race_result)
|
| 386 |
+
|
| 387 |
+
return results
|
| 388 |
+
|
| 389 |
+
# ═══════════════════════════════════════════════════════════════════
|
| 390 |
+
# MAIN
|
| 391 |
+
# ═══════════════════════════════════════════════════════════════════
|
| 392 |
+
def main():
|
| 393 |
+
parser = argparse.ArgumentParser()
|
| 394 |
+
parser.add_argument('--stage', choices=['cat1', 'cat7', 'cat8x', 'cat9x', 'cat10x', 'cat11', 'all'], default='all')
|
| 395 |
+
args = parser.parse_args()
|
| 396 |
+
|
| 397 |
+
all_results = {"timestamp": datetime.now(timezone.utc).isoformat(), "api_base": API_BASE}
|
| 398 |
+
|
| 399 |
+
# Health check
|
| 400 |
+
log(f"Health check: {API_BASE}")
|
| 401 |
+
try:
|
| 402 |
+
resp = requests.get(f"{API_BASE}/api/health", timeout=10)
|
| 403 |
+
log(f" OK: {resp.status_code}")
|
| 404 |
+
all_results['health'] = resp.json()
|
| 405 |
+
except Exception as e:
|
| 406 |
+
log(f" FAIL: {e}")
|
| 407 |
+
return
|
| 408 |
+
|
| 409 |
+
if args.stage in ('cat1', 'all'):
|
| 410 |
+
all_results['cat1'] = run_cat1()
|
| 411 |
+
if args.stage in ('cat7', 'all'):
|
| 412 |
+
all_results['cat7'] = run_cat7()
|
| 413 |
+
if args.stage in ('cat8x', 'all'):
|
| 414 |
+
all_results['cat8x'] = run_cat8_expanded()
|
| 415 |
+
if args.stage in ('cat9x', 'all'):
|
| 416 |
+
all_results['cat9x'] = run_cat9_expanded()
|
| 417 |
+
if args.stage in ('cat10x', 'all'):
|
| 418 |
+
all_results['cat10x'] = run_cat10_expanded()
|
| 419 |
+
if args.stage in ('cat11', 'all'):
|
| 420 |
+
all_results['cat11'] = run_cat11()
|
| 421 |
+
|
| 422 |
+
output_path = os.path.join(os.path.dirname(__file__), 'deep_dive_expanded.json')
|
| 423 |
+
with open(output_path, 'w', encoding='utf-8') as f:
|
| 424 |
+
json.dump(all_results, f, ensure_ascii=False, indent=2)
|
| 425 |
+
log(f"\nSaved to {output_path}")
|
| 426 |
+
|
| 427 |
+
if __name__ == '__main__':
|
| 428 |
+
main()
|
archive/old_tests/deep_dive_gaps.json
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"timestamp": "2026-06-20T19:33:59.110768+00:00",
|
| 3 |
+
"gap1_drift": {
|
| 4 |
+
"word_count": 187,
|
| 5 |
+
"char_count": 1104,
|
| 6 |
+
"total_suggestions": 0,
|
| 7 |
+
"front_half": 0,
|
| 8 |
+
"back_half": 0,
|
| 9 |
+
"coordinate_mismatches": [],
|
| 10 |
+
"a_spelling_changed": true,
|
| 11 |
+
"a_grammar_changed": true,
|
| 12 |
+
"a_punc_changed": true,
|
| 13 |
+
"suggestions": []
|
| 14 |
+
},
|
| 15 |
+
"gap2_priority": [
|
| 16 |
+
{
|
| 17 |
+
"id": "G2-01",
|
| 18 |
+
"input": "الطلاب اجتهدو في الامتحان",
|
| 19 |
+
"desc": "اجتهدو — spelling should add ا, grammar may do different fix. Grammar wins (priority 3 > 1)",
|
| 20 |
+
"a_spelling": "الطلاب اجتهدو في الامتحان",
|
| 21 |
+
"a_grammar": "الطلاب اجتهدو في الامتحان",
|
| 22 |
+
"a_punctuation": "الطلاب اجتهدو في الامتحان.",
|
| 23 |
+
"b_corrected": "الطلاب اجتهد في الامتحين",
|
| 24 |
+
"b_suggestions": [
|
| 25 |
+
{
|
| 26 |
+
"alternatives": [],
|
| 27 |
+
"confidence": 1.0,
|
| 28 |
+
"correction": "الامتحين",
|
| 29 |
+
"end": 25,
|
| 30 |
+
"id": "502647e7-18fd-41d2-b1c7-2978a3ee7704",
|
| 31 |
+
"locked": true,
|
| 32 |
+
"original": "الامتحان",
|
| 33 |
+
"priority": 3,
|
| 34 |
+
"start": 17,
|
| 35 |
+
"type": "grammar"
|
| 36 |
+
},
|
| 37 |
+
{
|
| 38 |
+
"alternatives": [
|
| 39 |
+
"اجتهد",
|
| 40 |
+
"اجتهدو"
|
| 41 |
+
],
|
| 42 |
+
"confidence": 0.9,
|
| 43 |
+
"correction": "اجتهد",
|
| 44 |
+
"end": 13,
|
| 45 |
+
"id": "0a93f071-af36-4219-b6f5-d11e748c4601",
|
| 46 |
+
"locked": true,
|
| 47 |
+
"original": "اجتهدو",
|
| 48 |
+
"priority": 1,
|
| 49 |
+
"start": 7,
|
| 50 |
+
"type": "spelling"
|
| 51 |
+
}
|
| 52 |
+
]
|
| 53 |
+
},
|
| 54 |
+
{
|
| 55 |
+
"id": "G2-02",
|
| 56 |
+
"input": "البنات ذهبو الى البيت",
|
| 57 |
+
"desc": "ذهبو — spelling could give ذهبوا, grammar could give ذهبن (fem). Grammar wins.",
|
| 58 |
+
"a_spelling": "البنات ذهبو إلى البيت",
|
| 59 |
+
"a_grammar": "البنات ذهبن الى البيت",
|
| 60 |
+
"a_punctuation": "البنات ذهبو الى البيت.",
|
| 61 |
+
"b_corrected": "البنات ذهبن إلى البيت.",
|
| 62 |
+
"b_suggestions": [
|
| 63 |
+
{
|
| 64 |
+
"alternatives": [],
|
| 65 |
+
"confidence": 1.0,
|
| 66 |
+
"correction": "ذهبن",
|
| 67 |
+
"end": 11,
|
| 68 |
+
"id": "0f00a9ab-1166-4e4d-8dd7-ae6dba1f9f1e",
|
| 69 |
+
"locked": true,
|
| 70 |
+
"original": "ذهبو",
|
| 71 |
+
"priority": 3,
|
| 72 |
+
"start": 7,
|
| 73 |
+
"type": "grammar"
|
| 74 |
+
},
|
| 75 |
+
{
|
| 76 |
+
"alternatives": [],
|
| 77 |
+
"confidence": 0.8,
|
| 78 |
+
"correction": "البيت.",
|
| 79 |
+
"end": 21,
|
| 80 |
+
"id": "af3a0a21-5e1e-45f5-a1ad-9c3730b4ab25",
|
| 81 |
+
"locked": true,
|
| 82 |
+
"original": "البيت",
|
| 83 |
+
"priority": 2,
|
| 84 |
+
"start": 16,
|
| 85 |
+
"type": "punctuation"
|
| 86 |
+
},
|
| 87 |
+
{
|
| 88 |
+
"alternatives": [
|
| 89 |
+
"إلى",
|
| 90 |
+
"ال",
|
| 91 |
+
"الم",
|
| 92 |
+
"الى"
|
| 93 |
+
],
|
| 94 |
+
"confidence": 0.9,
|
| 95 |
+
"correction": "إلى",
|
| 96 |
+
"end": 15,
|
| 97 |
+
"id": "1b7096dc-6043-4e1a-9de3-d59204327b86",
|
| 98 |
+
"locked": true,
|
| 99 |
+
"original": "الى",
|
| 100 |
+
"priority": 1,
|
| 101 |
+
"start": 12,
|
| 102 |
+
"type": "spelling"
|
| 103 |
+
}
|
| 104 |
+
]
|
| 105 |
+
},
|
| 106 |
+
{
|
| 107 |
+
"id": "G2-03",
|
| 108 |
+
"input": "وفجأه سقطت الكتب",
|
| 109 |
+
"desc": "وفجأه — spelling may fix ه→ة; punctuation may want comma after it. Overlap?",
|
| 110 |
+
"a_spelling": "وفجأه سقطت الكتب",
|
| 111 |
+
"a_grammar": "وفجأة سقطت الكتب",
|
| 112 |
+
"a_punctuation": "وفجأه سقطت الكتب.",
|
| 113 |
+
"b_corrected": "وفجأة سقطت الكتب.",
|
| 114 |
+
"b_suggestions": [
|
| 115 |
+
{
|
| 116 |
+
"alternatives": [],
|
| 117 |
+
"confidence": 0.8,
|
| 118 |
+
"correction": "الكتب.",
|
| 119 |
+
"end": 16,
|
| 120 |
+
"id": "fc257e46-4368-4d32-acb0-de5b6d461aaf",
|
| 121 |
+
"locked": true,
|
| 122 |
+
"original": "الكتب",
|
| 123 |
+
"priority": 2,
|
| 124 |
+
"start": 11,
|
| 125 |
+
"type": "punctuation"
|
| 126 |
+
},
|
| 127 |
+
{
|
| 128 |
+
"alternatives": [],
|
| 129 |
+
"confidence": 1.0,
|
| 130 |
+
"correction": "وفجأة",
|
| 131 |
+
"end": 5,
|
| 132 |
+
"id": "7397e7e6-e238-4ed4-a184-461f576a74f6",
|
| 133 |
+
"locked": true,
|
| 134 |
+
"original": "وفجأه",
|
| 135 |
+
"priority": 1,
|
| 136 |
+
"start": 0,
|
| 137 |
+
"type": "spelling"
|
| 138 |
+
}
|
| 139 |
+
]
|
| 140 |
+
}
|
| 141 |
+
],
|
| 142 |
+
"gap3_dropped": {
|
| 143 |
+
"tests": [
|
| 144 |
+
{
|
| 145 |
+
"input": "الطلاب الذين اجتهدو في دراستهم حققو نتائج ممتازه في الامتحانات",
|
| 146 |
+
"a_spell_diffs": [
|
| 147 |
+
{
|
| 148 |
+
"word_idx": 2,
|
| 149 |
+
"original": "اجتهدو",
|
| 150 |
+
"corrected": "اجتهد"
|
| 151 |
+
},
|
| 152 |
+
{
|
| 153 |
+
"word_idx": 5,
|
| 154 |
+
"original": "حققو",
|
| 155 |
+
"corrected": "حقوق"
|
| 156 |
+
}
|
| 157 |
+
],
|
| 158 |
+
"a_gram_diffs": [
|
| 159 |
+
{
|
| 160 |
+
"word_idx": 5,
|
| 161 |
+
"original": "حققو",
|
| 162 |
+
"corrected": "حققوا"
|
| 163 |
+
},
|
| 164 |
+
{
|
| 165 |
+
"word_idx": 7,
|
| 166 |
+
"original": "ممتازه",
|
| 167 |
+
"corrected": "ممتازة"
|
| 168 |
+
}
|
| 169 |
+
],
|
| 170 |
+
"a_punc_diffs": [
|
| 171 |
+
{
|
| 172 |
+
"word_idx": 9,
|
| 173 |
+
"original": "الامتحانات",
|
| 174 |
+
"corrected": "الامتحانات."
|
| 175 |
+
}
|
| 176 |
+
],
|
| 177 |
+
"b_suggestion_count": 4,
|
| 178 |
+
"dropped_spell": [],
|
| 179 |
+
"dropped_gram": [],
|
| 180 |
+
"dropped_punc": []
|
| 181 |
+
}
|
| 182 |
+
]
|
| 183 |
+
},
|
| 184 |
+
"gap4_rare": {
|
| 185 |
+
"tests": [
|
| 186 |
+
{
|
| 187 |
+
"id": "R-01",
|
| 188 |
+
"input": "استوقفني المشهد فتأملته مليا",
|
| 189 |
+
"domain": "literary",
|
| 190 |
+
"output": "استوقفني المشهد فتأملتة مليا",
|
| 191 |
+
"changed": true
|
| 192 |
+
},
|
| 193 |
+
{
|
| 194 |
+
"id": "R-02",
|
| 195 |
+
"input": "تستأثر القوى العظمى بالنفوذ الدولي",
|
| 196 |
+
"domain": "political_literary",
|
| 197 |
+
"output": "تستأثر القوى العظمى بالنفوذ الدولي",
|
| 198 |
+
"changed": false
|
| 199 |
+
},
|
| 200 |
+
{
|
| 201 |
+
"id": "R-03",
|
| 202 |
+
"input": "استقطب المؤتمر ثلة من العلماء الأفذاذ",
|
| 203 |
+
"domain": "formal_rare",
|
| 204 |
+
"output": "استقطب المؤتمر ثلة من العلماء الأفذاذ",
|
| 205 |
+
"changed": false
|
| 206 |
+
},
|
| 207 |
+
{
|
| 208 |
+
"id": "R-04",
|
| 209 |
+
"input": "يتسنى للمرء أن يستشف الحقيقة من بين السطور",
|
| 210 |
+
"domain": "literary_verb",
|
| 211 |
+
"output": "يتسنى للمرء أن يكتشف الحقيقة من بين السطور",
|
| 212 |
+
"changed": true
|
| 213 |
+
},
|
| 214 |
+
{
|
| 215 |
+
"id": "R-05",
|
| 216 |
+
"input": "ألقى المحاضر خطبة عصماء استحوذت على إعجاب الحاضرين",
|
| 217 |
+
"domain": "oratory",
|
| 218 |
+
"output": "ألقى المحاضر خطبة علماء استحوذت على إعجاب الحاضرين",
|
| 219 |
+
"changed": true
|
| 220 |
+
},
|
| 221 |
+
{
|
| 222 |
+
"id": "R-06",
|
| 223 |
+
"input": "تمخض الاجتماع عن قرارات مصيرية",
|
| 224 |
+
"domain": "formal_verb",
|
| 225 |
+
"output": "تمخض الاجتماع عن قرارات مصيرية",
|
| 226 |
+
"changed": false
|
| 227 |
+
},
|
| 228 |
+
{
|
| 229 |
+
"id": "R-07",
|
| 230 |
+
"input": "أرهقته المسغبة فاستكان للقدر",
|
| 231 |
+
"domain": "classical",
|
| 232 |
+
"output": "طريقتة المسببة فاستكان القدر",
|
| 233 |
+
"changed": true
|
| 234 |
+
},
|
| 235 |
+
{
|
| 236 |
+
"id": "R-08",
|
| 237 |
+
"input": "نستشرف آفاق المستقبل بثقة واقتدار",
|
| 238 |
+
"domain": "formal_speech",
|
| 239 |
+
"output": "نستشرف آفاق المستقبل بثقة واقتدار",
|
| 240 |
+
"changed": false
|
| 241 |
+
},
|
| 242 |
+
{
|
| 243 |
+
"id": "R-09",
|
| 244 |
+
"input": "اعتراه القلق فتملكه الأرق",
|
| 245 |
+
"domain": "literary_psych",
|
| 246 |
+
"output": "اعتراه القلق فتملكة الأرق",
|
| 247 |
+
"changed": true
|
| 248 |
+
},
|
| 249 |
+
{
|
| 250 |
+
"id": "R-10",
|
| 251 |
+
"input": "استأنف العمل بعد فترة من التقاعس",
|
| 252 |
+
"domain": "formal_verb",
|
| 253 |
+
"output": "استأنف العمل بعد فترة من التقاعد",
|
| 254 |
+
"changed": true
|
| 255 |
+
}
|
| 256 |
+
],
|
| 257 |
+
"fp_count": 6,
|
| 258 |
+
"total": 10
|
| 259 |
+
}
|
| 260 |
+
}
|
archive/old_tests/deep_dive_gaps.py
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Gap-filler tests for items explicitly requested in the prompt but not yet covered:
|
| 3 |
+
1. 200+ word cumulative drift test (Cat 10)
|
| 4 |
+
2. Lower-priority-wins limitation (Cat 4)
|
| 5 |
+
3. Systematic dropped patch logging (Cat 3)
|
| 6 |
+
4. Rare/literary vocabulary overcorrection (Cat 2)
|
| 7 |
+
"""
|
| 8 |
+
import sys, os, json, time, requests
|
| 9 |
+
from datetime import datetime, timezone
|
| 10 |
+
|
| 11 |
+
API_BASE = "https://bayan10-bayan-api.hf.space"
|
| 12 |
+
TIMEOUT = 60
|
| 13 |
+
|
| 14 |
+
def api_call(endpoint, text, retries=2):
|
| 15 |
+
url = f"{API_BASE}{endpoint}"
|
| 16 |
+
for attempt in range(retries + 1):
|
| 17 |
+
try:
|
| 18 |
+
t0 = time.time()
|
| 19 |
+
resp = requests.post(url, json={"text": text}, timeout=TIMEOUT)
|
| 20 |
+
elapsed = int((time.time() - t0) * 1000)
|
| 21 |
+
if resp.status_code == 200:
|
| 22 |
+
data = resp.json()
|
| 23 |
+
data['_elapsed_ms'] = elapsed
|
| 24 |
+
return data
|
| 25 |
+
else:
|
| 26 |
+
if attempt < retries:
|
| 27 |
+
time.sleep(2)
|
| 28 |
+
continue
|
| 29 |
+
return {"error": f"HTTP {resp.status_code}: {resp.text[:200]}", "_elapsed_ms": elapsed}
|
| 30 |
+
except Exception as e:
|
| 31 |
+
return {"error": str(e)}
|
| 32 |
+
|
| 33 |
+
def log(msg):
|
| 34 |
+
print(f"[{datetime.now().strftime('%H:%M:%S')}] {msg}", flush=True)
|
| 35 |
+
|
| 36 |
+
results = {"timestamp": datetime.now(timezone.utc).isoformat()}
|
| 37 |
+
|
| 38 |
+
# ═══════════════════════════════════════════════════════════════
|
| 39 |
+
# GAP 1: 200+ word cumulative drift test (Cat 10)
|
| 40 |
+
# ═══════════════════════════════════════════════════════════════
|
| 41 |
+
log("=== GAP 1: 200+ word cumulative drift test ===")
|
| 42 |
+
|
| 43 |
+
# Build a 200+ word paragraph with deliberate errors throughout
|
| 44 |
+
long_para = (
|
| 45 |
+
"كانت الفتيات يلعبون في الحديقه الجميله وفجأه سقطت احداهن وبدءت تبكي بشده "
|
| 46 |
+
"ذهب الولد الى المدرسه وقابل المعلمه واخذ الكتاب وبدأ يقرأ بتركيز شديد "
|
| 47 |
+
"ان الذكاء الاصطناعي يلعب دورا هاما في تطوير التكنولوجيا الحديثه ولذالك يجب الاهتمام بة "
|
| 48 |
+
"هذة المدينه جميله جدا ومناخها معتدل طوال العام وسكانها طيبون ومحبون للخير "
|
| 49 |
+
"الطلاب الذين اجتهدو في دراستهم حققو نتائج ممتازه في الامتحانات النهائيه "
|
| 50 |
+
"سافر محمد إلى دبي للعمل في شركة جوجل وقابل أصدقاءه القدامى هناك "
|
| 51 |
+
"يستخدم الذكاء الاصطناعي تقنيات التعلم العميق في معالجة البيانات الضخمة والتحليل "
|
| 52 |
+
"القاهرة عاصمة جمهورية مصر العربية وأكبر مدنها وتقع على ضفاف نهر النيل العظيم "
|
| 53 |
+
"تتراوح درجات الحرارة بين خمس وعشرين وثلاثين درجة مئوية في فصل الصيف الحار "
|
| 54 |
+
"الحمد لله رب العالمين الرحمن الرحيم مالك يوم الدين إياك نعبد وإياك نستعين "
|
| 55 |
+
"بسم الله الرحمن الرحيم نبدأ هذة المحاضره عن اهمية التعليم في حياة الانسان "
|
| 56 |
+
"يحب الأطفال اللعب في الحديقة وركوب الدراجات والجري بين الأشجار والزهور الجميلة "
|
| 57 |
+
"إن العلم نور والجهل ظلام فاحرصوا على طلب العلم من المهد إلى اللحد "
|
| 58 |
+
"كان الرجل يمشي في الشارع وفجأه رأى صديقه القديم فسلم عليه وتحدثا طويلا "
|
| 59 |
+
"المعلم الذي يحب عمله يجتهد في تعليم طلابه ويحرص على نجاحهم في الحياه "
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
word_count = len(long_para.split())
|
| 63 |
+
char_count = len(long_para)
|
| 64 |
+
log(f" Input: {word_count} words, {char_count} chars")
|
| 65 |
+
|
| 66 |
+
# Track A: each model on the full long text
|
| 67 |
+
log(" Running Track A (each model independently on original)...")
|
| 68 |
+
a_spell = api_call("/api/spelling", long_para)
|
| 69 |
+
a_gram = api_call("/api/grammar", long_para)
|
| 70 |
+
a_punc = api_call("/api/punctuation", long_para)
|
| 71 |
+
|
| 72 |
+
# Track B: full pipeline
|
| 73 |
+
log(" Running Track B (full pipeline)...")
|
| 74 |
+
b = api_call("/api/analyze", long_para)
|
| 75 |
+
|
| 76 |
+
sugg = b.get("suggestions", [])
|
| 77 |
+
mid_char = char_count // 2
|
| 78 |
+
|
| 79 |
+
# Verify ALL coordinates
|
| 80 |
+
coord_mismatches = []
|
| 81 |
+
for s in sugg:
|
| 82 |
+
start, end = s.get('start', 0), s.get('end', 0)
|
| 83 |
+
expected_text = long_para[start:end]
|
| 84 |
+
actual_text = s.get('original', '')
|
| 85 |
+
if expected_text != actual_text:
|
| 86 |
+
coord_mismatches.append({
|
| 87 |
+
"start": start, "end": end,
|
| 88 |
+
"expected": expected_text,
|
| 89 |
+
"actual": actual_text,
|
| 90 |
+
"correction": s.get('correction', ''),
|
| 91 |
+
"type": s.get('type', ''),
|
| 92 |
+
})
|
| 93 |
+
|
| 94 |
+
back_half = [s for s in sugg if s.get('start', 0) >= mid_char]
|
| 95 |
+
front_half = [s for s in sugg if s.get('start', 0) < mid_char]
|
| 96 |
+
|
| 97 |
+
# Log every suggestion with its verified coordinate
|
| 98 |
+
log(f" Total: {len(sugg)} suggestions, {len(coord_mismatches)} coordinate mismatches")
|
| 99 |
+
log(f" Front half ({mid_char} chars): {len(front_half)} suggestions")
|
| 100 |
+
log(f" Back half: {len(back_half)} suggestions")
|
| 101 |
+
for s in sugg:
|
| 102 |
+
st, en = s.get('start',0), s.get('end',0)
|
| 103 |
+
in_back = "BACK" if st >= mid_char else "FRONT"
|
| 104 |
+
verified = "✓" if long_para[st:en] == s.get('original','') else "✗ MISMATCH"
|
| 105 |
+
log(f" [{in_back}] [{st}:{en}] '{s.get('original','')}' → '{s.get('correction','')}' ({s.get('type','')}) {verified}")
|
| 106 |
+
|
| 107 |
+
for m in coord_mismatches:
|
| 108 |
+
log(f" MISMATCH: [{m['start']}:{m['end']}] expected='{m['expected']}' actual='{m['actual']}'")
|
| 109 |
+
|
| 110 |
+
results['gap1_drift'] = {
|
| 111 |
+
"word_count": word_count, "char_count": char_count,
|
| 112 |
+
"total_suggestions": len(sugg),
|
| 113 |
+
"front_half": len(front_half), "back_half": len(back_half),
|
| 114 |
+
"coordinate_mismatches": coord_mismatches,
|
| 115 |
+
"a_spelling_changed": a_spell.get("corrected_text","") != long_para,
|
| 116 |
+
"a_grammar_changed": a_gram.get("corrected_text","") != long_para,
|
| 117 |
+
"a_punc_changed": a_punc.get("corrected_text","") != long_para,
|
| 118 |
+
"suggestions": sugg,
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
# ═══════════════════════════════════════════════════════════════
|
| 122 |
+
# GAP 2: Lower-priority-wins limitation doc (Cat 4)
|
| 123 |
+
# ═══════════════════════════════════════════════════════════════
|
| 124 |
+
log("\n=== GAP 2: Lower-priority stage was more important (Cat 4) ===")
|
| 125 |
+
|
| 126 |
+
# Construct case: spelling corrects اجتهدو→اجتهدوا (correct, priority 1)
|
| 127 |
+
# but grammar might also touch it with a different correction (priority 3)
|
| 128 |
+
# Grammar WINS because higher priority. But what if grammar is wrong here?
|
| 129 |
+
gap2_tests = [
|
| 130 |
+
{
|
| 131 |
+
"id": "G2-01",
|
| 132 |
+
"input": "الطلاب اجتهدو في الامتحان",
|
| 133 |
+
"desc": "اجتهدو — spelling should add ا, grammar may do different fix. Grammar wins (priority 3 > 1)",
|
| 134 |
+
},
|
| 135 |
+
{
|
| 136 |
+
"id": "G2-02",
|
| 137 |
+
"input": "البنات ذهبو الى البيت",
|
| 138 |
+
"desc": "ذهبو — spelling could give ذهبوا, grammar could give ذهبن (fem). Grammar wins.",
|
| 139 |
+
},
|
| 140 |
+
{
|
| 141 |
+
"id": "G2-03",
|
| 142 |
+
"input": "وفجأه سقطت الكتب",
|
| 143 |
+
"desc": "وفجأه — spelling may fix ه→ة; punctuation may want comma after it. Overlap?",
|
| 144 |
+
},
|
| 145 |
+
]
|
| 146 |
+
|
| 147 |
+
for test in gap2_tests:
|
| 148 |
+
log(f" {test['id']}: {test['input']}")
|
| 149 |
+
a_sp = api_call("/api/spelling", test['input'])
|
| 150 |
+
a_gr = api_call("/api/grammar", test['input'])
|
| 151 |
+
a_pu = api_call("/api/punctuation", test['input'])
|
| 152 |
+
b = api_call("/api/analyze", test['input'])
|
| 153 |
+
|
| 154 |
+
a_sp_out = a_sp.get("corrected_text", test['input'])
|
| 155 |
+
a_gr_out = a_gr.get("corrected_text", test['input'])
|
| 156 |
+
a_pu_out = a_pu.get("corrected_text", test['input'])
|
| 157 |
+
|
| 158 |
+
log(f" A_spell: {a_sp_out}")
|
| 159 |
+
log(f" A_gram: {a_gr_out}")
|
| 160 |
+
log(f" A_punc: {a_pu_out}")
|
| 161 |
+
log(f" B_final: {b.get('corrected','')}")
|
| 162 |
+
log(f" B_sugg: {len(b.get('suggestions',[]))}")
|
| 163 |
+
|
| 164 |
+
# Which stage's correction won for each word?
|
| 165 |
+
b_sugg = b.get('suggestions', [])
|
| 166 |
+
for s in b_sugg:
|
| 167 |
+
log(f" [{s.get('type','')}] [{s.get('start',0)}:{s.get('end',0)}] '{s.get('original','')}' → '{s.get('correction','')}'")
|
| 168 |
+
|
| 169 |
+
test['a_spelling'] = a_sp_out
|
| 170 |
+
test['a_grammar'] = a_gr_out
|
| 171 |
+
test['a_punctuation'] = a_pu_out
|
| 172 |
+
test['b_corrected'] = b.get('corrected', '')
|
| 173 |
+
test['b_suggestions'] = b_sugg
|
| 174 |
+
|
| 175 |
+
results['gap2_priority'] = gap2_tests
|
| 176 |
+
|
| 177 |
+
# ═══════════════════════════════════════════════════════════════
|
| 178 |
+
# GAP 3: Systematic dropped patch logging (Cat 3)
|
| 179 |
+
# ═══════════════════════════════════════════════════════════════
|
| 180 |
+
log("\n=== GAP 3: Systematic dropped patch comparison (Cat 3) ===")
|
| 181 |
+
|
| 182 |
+
# For each test: run all 3 models independently, count expected patches,
|
| 183 |
+
# compare with actual Track B patches. Any patch Track A produces but
|
| 184 |
+
# Track B doesn't = dropped patch.
|
| 185 |
+
gap3_tests = [
|
| 186 |
+
"كانت الفتيات يلعبون في الحديقه وفجأه سقطت احداهن وبدءت تبكي بشده",
|
| 187 |
+
"ان الذكاء الاصطناعي يلعب دورا هاما ولذالك يجب الاهتمام بة",
|
| 188 |
+
"هذة المدينه جميله جدا ومناخها معتدل طوال العام",
|
| 189 |
+
"ذهب الولد الى المكتبه وقرا كتاب مفيد",
|
| 190 |
+
"الطلاب الذين اجتهدو في دراستهم حققو نتائج ممتازه في الامتحانات",
|
| 191 |
+
]
|
| 192 |
+
|
| 193 |
+
for i, text in enumerate(gap3_tests):
|
| 194 |
+
log(f" Test {i+1}: {text[:50]}...")
|
| 195 |
+
a_sp = api_call("/api/spelling", text)
|
| 196 |
+
a_gr = api_call("/api/grammar", text)
|
| 197 |
+
a_pu = api_call("/api/punctuation", text)
|
| 198 |
+
b = api_call("/api/analyze", text)
|
| 199 |
+
|
| 200 |
+
a_sp_out = a_sp.get("corrected_text", text)
|
| 201 |
+
a_gr_out = a_gr.get("corrected_text", text)
|
| 202 |
+
a_pu_out = a_pu.get("corrected_text", text)
|
| 203 |
+
|
| 204 |
+
# Find word-level changes from each model
|
| 205 |
+
def word_diffs(orig, corrected):
|
| 206 |
+
o_words = orig.split()
|
| 207 |
+
c_words = corrected.split()
|
| 208 |
+
diffs = []
|
| 209 |
+
for j, (ow, cw) in enumerate(zip(o_words, c_words)):
|
| 210 |
+
if ow != cw:
|
| 211 |
+
diffs.append({"word_idx": j, "original": ow, "corrected": cw})
|
| 212 |
+
return diffs
|
| 213 |
+
|
| 214 |
+
sp_diffs = word_diffs(text, a_sp_out)
|
| 215 |
+
gr_diffs = word_diffs(text, a_gr_out)
|
| 216 |
+
pu_diffs = word_diffs(text, a_pu_out)
|
| 217 |
+
|
| 218 |
+
b_sugg = b.get('suggestions', [])
|
| 219 |
+
b_corrections = set()
|
| 220 |
+
for s in b_sugg:
|
| 221 |
+
b_corrections.add(s.get('original', ''))
|
| 222 |
+
|
| 223 |
+
# Track A produced these corrections; check which survived to Track B
|
| 224 |
+
dropped_spell = [d for d in sp_diffs if d['original'] not in b_corrections and d['corrected'] != d['original']]
|
| 225 |
+
dropped_gram = [d for d in gr_diffs if d['original'] not in b_corrections and d['corrected'] != d['original']]
|
| 226 |
+
dropped_punc = [d for d in pu_diffs if d['original'] not in b_corrections and d['corrected'] != d['original']]
|
| 227 |
+
|
| 228 |
+
log(f" Track A changes: spell={len(sp_diffs)}, gram={len(gr_diffs)}, punc={len(pu_diffs)}")
|
| 229 |
+
log(f" Track B suggestions: {len(b_sugg)}")
|
| 230 |
+
log(f" Dropped: spell={len(dropped_spell)}, gram={len(dropped_gram)}, punc={len(dropped_punc)}")
|
| 231 |
+
|
| 232 |
+
for d in dropped_spell:
|
| 233 |
+
log(f" DROPPED SPELL: '{d['original']}' → '{d['corrected']}' (reason: likely filter blocked)")
|
| 234 |
+
for d in dropped_gram:
|
| 235 |
+
log(f" DROPPED GRAM: '{d['original']}' → '{d['corrected']}' (reason: likely StageLocker)")
|
| 236 |
+
for d in dropped_punc:
|
| 237 |
+
log(f" DROPPED PUNC: '{d['original']}' → '{d['corrected']}' (reason: likely lock/cap/safety)")
|
| 238 |
+
|
| 239 |
+
results[f'gap3_dropped'] = {
|
| 240 |
+
"tests": [
|
| 241 |
+
{
|
| 242 |
+
"input": text,
|
| 243 |
+
"a_spell_diffs": word_diffs(text, api_call("/api/spelling", text).get("corrected_text", text)) if False else sp_diffs,
|
| 244 |
+
"a_gram_diffs": gr_diffs,
|
| 245 |
+
"a_punc_diffs": pu_diffs,
|
| 246 |
+
"b_suggestion_count": len(b_sugg),
|
| 247 |
+
"dropped_spell": dropped_spell,
|
| 248 |
+
"dropped_gram": dropped_gram,
|
| 249 |
+
"dropped_punc": dropped_punc,
|
| 250 |
+
}
|
| 251 |
+
for text, sp_diffs, gr_diffs, pu_diffs, b_sugg in [(text, sp_diffs, gr_diffs, pu_diffs, b_sugg)]
|
| 252 |
+
]
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
# ═══════════════════════════════════════════════════════════════
|
| 256 |
+
# GAP 4: Rare/literary vocabulary (Cat 2)
|
| 257 |
+
# ═══════════════════════════════════════════════════════════════
|
| 258 |
+
log("\n=== GAP 4: Rare/literary vocabulary overcorrection (Cat 2) ===")
|
| 259 |
+
|
| 260 |
+
rare_tests = [
|
| 261 |
+
{"id": "R-01", "input": "استوقفني المشهد فتأملته مليا", "domain": "literary"},
|
| 262 |
+
{"id": "R-02", "input": "تستأثر القوى العظمى بالنفوذ الدولي", "domain": "political_literary"},
|
| 263 |
+
{"id": "R-03", "input": "استقطب المؤتمر ثلة من العلماء الأفذاذ", "domain": "formal_rare"},
|
| 264 |
+
{"id": "R-04", "input": "يتسنى للمرء أن يستشف الحقيقة من بين السطور", "domain": "literary_verb"},
|
| 265 |
+
{"id": "R-05", "input": "ألقى المحاضر خطبة عصماء استحوذت على إعجاب الحاضرين", "domain": "oratory"},
|
| 266 |
+
{"id": "R-06", "input": "تمخض الاجتماع عن قرارات مصيرية", "domain": "formal_verb"},
|
| 267 |
+
{"id": "R-07", "input": "أرهقته المسغبة فاستكان للقدر", "domain": "classical"},
|
| 268 |
+
{"id": "R-08", "input": "نستشرف آفاق المستقبل بثقة واقتدار", "domain": "formal_speech"},
|
| 269 |
+
{"id": "R-09", "input": "اعتراه القلق فتملكه الأرق", "domain": "literary_psych"},
|
| 270 |
+
{"id": "R-10", "input": "استأنف العمل بعد فترة من التقاعس", "domain": "formal_verb"},
|
| 271 |
+
]
|
| 272 |
+
|
| 273 |
+
fp_count = 0
|
| 274 |
+
for test in rare_tests:
|
| 275 |
+
a = api_call("/api/spelling", test['input'])
|
| 276 |
+
a_out = a.get("corrected_text", test['input'])
|
| 277 |
+
changed = a_out != test['input']
|
| 278 |
+
if changed:
|
| 279 |
+
fp_count += 1
|
| 280 |
+
log(f" ⚠ {test['id']}: '{test['input'][:40]}...' → '{a_out[:40]}...' [{test['domain']}]")
|
| 281 |
+
else:
|
| 282 |
+
log(f" ✓ {test['id']}: no change [{test['domain']}]")
|
| 283 |
+
test['output'] = a_out
|
| 284 |
+
test['changed'] = changed
|
| 285 |
+
|
| 286 |
+
log(f" Rare/literary FP rate: {fp_count}/{len(rare_tests)} ({fp_count*100//len(rare_tests)}%)")
|
| 287 |
+
results['gap4_rare'] = {"tests": rare_tests, "fp_count": fp_count, "total": len(rare_tests)}
|
| 288 |
+
|
| 289 |
+
# ═══════════════════════════════════════════════════════════════
|
| 290 |
+
# SAVE
|
| 291 |
+
# ═══════════════════════════════════════════════════════════════
|
| 292 |
+
output_path = os.path.join(os.path.dirname(__file__), 'deep_dive_gaps.json')
|
| 293 |
+
with open(output_path, 'w', encoding='utf-8') as f:
|
| 294 |
+
json.dump(results, f, ensure_ascii=False, indent=2)
|
| 295 |
+
log(f"\nSaved to {output_path}")
|
archive/old_tests/deep_dive_output.json
ADDED
|
@@ -0,0 +1,671 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"timestamp": "2026-06-20T19:00:06.993902+00:00",
|
| 3 |
+
"api_base": "https://bayan10-bayan-api.hf.space",
|
| 4 |
+
"health": {
|
| 5 |
+
"environment": "huggingface_spaces",
|
| 6 |
+
"mode": "hf_spaces_local",
|
| 7 |
+
"models": {
|
| 8 |
+
"autocomplete": true,
|
| 9 |
+
"grammar": true,
|
| 10 |
+
"punctuation": true,
|
| 11 |
+
"spelling": true,
|
| 12 |
+
"summarization": true
|
| 13 |
+
},
|
| 14 |
+
"note": "Free tier: summarization local, other models return input unchanged",
|
| 15 |
+
"status": "healthy",
|
| 16 |
+
"supabase": {
|
| 17 |
+
"configured": true
|
| 18 |
+
}
|
| 19 |
+
},
|
| 20 |
+
"pipeline_tests": [
|
| 21 |
+
{
|
| 22 |
+
"id": "C3-01",
|
| 23 |
+
"category": 3,
|
| 24 |
+
"input": "كانت الفتيات يلعبون في الحديقه وفجأه سقطت احداهن وبدءت تبكي بشده",
|
| 25 |
+
"track_a": {
|
| 26 |
+
"spelling": "كانت الفتيات يلعبون في الحديقه وفجأه سقطت احداهن وبدءت تبكي بشدة",
|
| 27 |
+
"spelling_changed": true,
|
| 28 |
+
"grammar": "كانت الفتيات يلعبن في الحديقة وفجأة سقطت إحدىهن وبدأت تبكي بشدة",
|
| 29 |
+
"grammar_changed": true,
|
| 30 |
+
"punctuation": "كانت الفتيات يلعبون في الحديقه وفجأه، سقطت احداهن وبدءت تبكي بشده",
|
| 31 |
+
"punctuation_changed": true
|
| 32 |
+
},
|
| 33 |
+
"track_b": {
|
| 34 |
+
"corrected": "كانت الفتيات يلعبن في الحديقة وفجأة سقطت إحدىهن وبدأت تبكي بشدة.",
|
| 35 |
+
"suggestions": [
|
| 36 |
+
{
|
| 37 |
+
"alternatives": [],
|
| 38 |
+
"confidence": 1.0,
|
| 39 |
+
"correction": "يلعبن",
|
| 40 |
+
"end": 19,
|
| 41 |
+
"id": "e984c773-8d33-4a30-b5b8-49cee91e1095",
|
| 42 |
+
"locked": true,
|
| 43 |
+
"original": "يلعبون",
|
| 44 |
+
"priority": 3,
|
| 45 |
+
"start": 13,
|
| 46 |
+
"type": "grammar"
|
| 47 |
+
},
|
| 48 |
+
{
|
| 49 |
+
"alternatives": [],
|
| 50 |
+
"confidence": 1.0,
|
| 51 |
+
"correction": "إحدىهن وبدأت",
|
| 52 |
+
"end": 54,
|
| 53 |
+
"id": "38054ed7-9bd2-4e04-9314-b4a63b84ad07",
|
| 54 |
+
"locked": true,
|
| 55 |
+
"original": "احداهن وبدءت",
|
| 56 |
+
"priority": 3,
|
| 57 |
+
"start": 42,
|
| 58 |
+
"type": "grammar"
|
| 59 |
+
},
|
| 60 |
+
{
|
| 61 |
+
"alternatives": [],
|
| 62 |
+
"confidence": 0.8,
|
| 63 |
+
"correction": "بشدة.",
|
| 64 |
+
"end": 64,
|
| 65 |
+
"id": "16e72e95-6326-4365-a0f3-ad2602bcfc49",
|
| 66 |
+
"locked": true,
|
| 67 |
+
"original": "بشده",
|
| 68 |
+
"priority": 2,
|
| 69 |
+
"start": 60,
|
| 70 |
+
"type": "punctuation"
|
| 71 |
+
},
|
| 72 |
+
{
|
| 73 |
+
"alternatives": [],
|
| 74 |
+
"confidence": 1.0,
|
| 75 |
+
"correction": "الحديقة وفجأة",
|
| 76 |
+
"end": 36,
|
| 77 |
+
"id": "1de0b7c2-e2e5-45e3-8ba3-6fe062ee8fcc",
|
| 78 |
+
"locked": true,
|
| 79 |
+
"original": "الحديقه وفجأه",
|
| 80 |
+
"priority": 1,
|
| 81 |
+
"start": 23,
|
| 82 |
+
"type": "spelling"
|
| 83 |
+
}
|
| 84 |
+
],
|
| 85 |
+
"timing_ms": {
|
| 86 |
+
"grammar_ms": 4561,
|
| 87 |
+
"punctuation_ms": 1492,
|
| 88 |
+
"spelling_ms": 1529,
|
| 89 |
+
"total_ms": 7587
|
| 90 |
+
}
|
| 91 |
+
}
|
| 92 |
+
},
|
| 93 |
+
{
|
| 94 |
+
"id": "C3-02",
|
| 95 |
+
"category": 3,
|
| 96 |
+
"input": "ان الذكاء الاصطناعي يلعب دورا هاما ولذالك يجب الاهتمام بة",
|
| 97 |
+
"track_a": {
|
| 98 |
+
"spelling": "ان الذكاء الاصطناعي يلعب دورا هاما ولذالك يجب الاهتمام بة",
|
| 99 |
+
"spelling_changed": false,
|
| 100 |
+
"grammar": "ان الذكاء الاصطناعي يلعب دورا هاما ولذلك يجب الاهتمام به",
|
| 101 |
+
"grammar_changed": true,
|
| 102 |
+
"punctuation": "ان الذكاء الاصطناعي يلعب دورا هاما ولذالك؛ يجب الاهتمام بة",
|
| 103 |
+
"punctuation_changed": true
|
| 104 |
+
},
|
| 105 |
+
"track_b": {
|
| 106 |
+
"corrected": "ان الذكاء الاصطناعي يلعب دورا هاما ولذلك يجب الاهتمام به",
|
| 107 |
+
"suggestions": [
|
| 108 |
+
{
|
| 109 |
+
"alternatives": [],
|
| 110 |
+
"confidence": 1.0,
|
| 111 |
+
"correction": "ولذلك",
|
| 112 |
+
"end": 41,
|
| 113 |
+
"id": "9870eb8d-0bf7-4a58-90cb-940b5475a37e",
|
| 114 |
+
"locked": true,
|
| 115 |
+
"original": "ولذالك",
|
| 116 |
+
"priority": 3,
|
| 117 |
+
"start": 35,
|
| 118 |
+
"type": "grammar"
|
| 119 |
+
},
|
| 120 |
+
{
|
| 121 |
+
"alternatives": [],
|
| 122 |
+
"confidence": 1.0,
|
| 123 |
+
"correction": "به",
|
| 124 |
+
"end": 57,
|
| 125 |
+
"id": "ea9f3fca-eee1-4597-8f4a-00f50558d510",
|
| 126 |
+
"locked": true,
|
| 127 |
+
"original": "بة",
|
| 128 |
+
"priority": 1,
|
| 129 |
+
"start": 55,
|
| 130 |
+
"type": "spelling"
|
| 131 |
+
}
|
| 132 |
+
],
|
| 133 |
+
"timing_ms": {
|
| 134 |
+
"grammar_ms": 1304,
|
| 135 |
+
"punctuation_ms": 1050,
|
| 136 |
+
"spelling_ms": 1193,
|
| 137 |
+
"total_ms": 3549
|
| 138 |
+
}
|
| 139 |
+
}
|
| 140 |
+
},
|
| 141 |
+
{
|
| 142 |
+
"id": "C3-03",
|
| 143 |
+
"category": 3,
|
| 144 |
+
"input": "التزم الر��اضي بتناول وجباته الصحية وحساب سعراته بدقة رغبة في بناء كتلة عضلية قوية ويا له من التزام حديدي يثير الإعجاب",
|
| 145 |
+
"track_a": {
|
| 146 |
+
"spelling": "التزم الرياضي بتناول وجبات الصحية وحساب سعادتة بدقة رغبة في بناء كتلة عملية قوية ويا له من التزام حديدي يثير الإعجاب",
|
| 147 |
+
"spelling_changed": true,
|
| 148 |
+
"grammar": "التزم الرياضي بتناول وجباته الصحية وحساب سعراته بدقة رغبة في بناء كتلة عضلية قوية ويا له من التزام حديدي يثير الإعجاب",
|
| 149 |
+
"grammar_changed": false,
|
| 150 |
+
"punctuation": "التزم الرياضي بتناول وجباته الصحية وحساب سعراته بدقة رغبة؛ في بناء كتلة عضلية قوية ويا له، من التزام حديدي يثير الإعجاب",
|
| 151 |
+
"punctuation_changed": true
|
| 152 |
+
},
|
| 153 |
+
"track_b": {
|
| 154 |
+
"corrected": "التزم الرياضي بتناول وجباته الصحية وحساب سعراته بدقة رغبة؛ في بناء كتلة عضلية قوية ويا له، من التزام حديدي يثير الإعجاب",
|
| 155 |
+
"suggestions": [
|
| 156 |
+
{
|
| 157 |
+
"alternatives": [],
|
| 158 |
+
"confidence": 0.8,
|
| 159 |
+
"correction": "رغبة؛",
|
| 160 |
+
"end": 57,
|
| 161 |
+
"id": "b7e29bf0-2565-4b46-b815-58e1b56717c1",
|
| 162 |
+
"locked": true,
|
| 163 |
+
"original": "رغبة",
|
| 164 |
+
"priority": 2,
|
| 165 |
+
"start": 53,
|
| 166 |
+
"type": "punctuation"
|
| 167 |
+
},
|
| 168 |
+
{
|
| 169 |
+
"alternatives": [],
|
| 170 |
+
"confidence": 0.8,
|
| 171 |
+
"correction": "له،",
|
| 172 |
+
"end": 88,
|
| 173 |
+
"id": "6d1e2b65-d2a4-41f1-a803-ce06e93e79c4",
|
| 174 |
+
"locked": true,
|
| 175 |
+
"original": "له",
|
| 176 |
+
"priority": 2,
|
| 177 |
+
"start": 86,
|
| 178 |
+
"type": "punctuation"
|
| 179 |
+
}
|
| 180 |
+
],
|
| 181 |
+
"timing_ms": {
|
| 182 |
+
"grammar_ms": 6012,
|
| 183 |
+
"punctuation_ms": 2080,
|
| 184 |
+
"spelling_ms": 2197,
|
| 185 |
+
"total_ms": 10291
|
| 186 |
+
}
|
| 187 |
+
}
|
| 188 |
+
},
|
| 189 |
+
{
|
| 190 |
+
"id": "C3-04",
|
| 191 |
+
"category": 3,
|
| 192 |
+
"input": "هذة المدينه جميله جدا ومناخها معتدل طوال العام",
|
| 193 |
+
"track_a": {
|
| 194 |
+
"spelling": "هذة المدينه جميله جدا ومناخها معتدل طوال العام",
|
| 195 |
+
"spelling_changed": false,
|
| 196 |
+
"grammar": "هذه المدينة جميلة جدا ومناخها معتدل طوال العام",
|
| 197 |
+
"grammar_changed": true,
|
| 198 |
+
"punctuation": "هذة المدينه جميله جدا ومناخها معتدل طوال العام.",
|
| 199 |
+
"punctuation_changed": true
|
| 200 |
+
},
|
| 201 |
+
"track_b": {
|
| 202 |
+
"corrected": "هذه المدينة جميلة جدا ومناخها معتدل طوال العام.",
|
| 203 |
+
"suggestions": [
|
| 204 |
+
{
|
| 205 |
+
"alternatives": [],
|
| 206 |
+
"confidence": 0.8,
|
| 207 |
+
"correction": "العام.",
|
| 208 |
+
"end": 46,
|
| 209 |
+
"id": "a4de368f-4ae7-451a-bbe2-ff7fca6b3f3b",
|
| 210 |
+
"locked": true,
|
| 211 |
+
"original": "العام",
|
| 212 |
+
"priority": 2,
|
| 213 |
+
"start": 41,
|
| 214 |
+
"type": "punctuation"
|
| 215 |
+
},
|
| 216 |
+
{
|
| 217 |
+
"alternatives": [],
|
| 218 |
+
"confidence": 1.0,
|
| 219 |
+
"correction": "هذه المدينة جميلة",
|
| 220 |
+
"end": 17,
|
| 221 |
+
"id": "9ff77094-1e33-4946-a343-317f51b8b539",
|
| 222 |
+
"locked": true,
|
| 223 |
+
"original": "هذة المدينه جميله",
|
| 224 |
+
"priority": 1,
|
| 225 |
+
"start": 0,
|
| 226 |
+
"type": "spelling"
|
| 227 |
+
}
|
| 228 |
+
],
|
| 229 |
+
"timing_ms": {
|
| 230 |
+
"grammar_ms": 1461,
|
| 231 |
+
"punctuation_ms": 804,
|
| 232 |
+
"spelling_ms": 970,
|
| 233 |
+
"total_ms": 3236
|
| 234 |
+
}
|
| 235 |
+
}
|
| 236 |
+
},
|
| 237 |
+
{
|
| 238 |
+
"id": "C3-05",
|
| 239 |
+
"category": 3,
|
| 240 |
+
"input": "الطلاب الذين اجتهدو في دراستهم حققو نتائج ممتازه في الامتحانات",
|
| 241 |
+
"track_a": {
|
| 242 |
+
"spelling": "الطلاب الذين اجتهد في دراستهم حقوق نتائج ممتازه في الامتحانات",
|
| 243 |
+
"spelling_changed": true,
|
| 244 |
+
"grammar": "الطلاب الذين اجتهدو في دراستهم حققو نتائج ممتازه في الامتحانات",
|
| 245 |
+
"grammar_changed": false,
|
| 246 |
+
"punctuation": "الطلاب الذين اجتهدو في دراستهم حققو نتائج ممتازه في الامتحانات",
|
| 247 |
+
"punctuation_changed": false
|
| 248 |
+
},
|
| 249 |
+
"track_b": {
|
| 250 |
+
"corrected": "",
|
| 251 |
+
"suggestions": [],
|
| 252 |
+
"timing_ms": {}
|
| 253 |
+
}
|
| 254 |
+
},
|
| 255 |
+
{
|
| 256 |
+
"id": "C4-01",
|
| 257 |
+
"category": 4,
|
| 258 |
+
"input": "كانت الفتيات يلعبون في الحديقه",
|
| 259 |
+
"runs": [
|
| 260 |
+
{
|
| 261 |
+
"run": 1,
|
| 262 |
+
"corrected": "",
|
| 263 |
+
"suggestions": []
|
| 264 |
+
},
|
| 265 |
+
{
|
| 266 |
+
"run": 2,
|
| 267 |
+
"corrected": "",
|
| 268 |
+
"suggestions": []
|
| 269 |
+
},
|
| 270 |
+
{
|
| 271 |
+
"run": 3,
|
| 272 |
+
"corrected": "",
|
| 273 |
+
"suggestions": []
|
| 274 |
+
}
|
| 275 |
+
],
|
| 276 |
+
"deterministic": true
|
| 277 |
+
},
|
| 278 |
+
{
|
| 279 |
+
"id": "C4-02",
|
| 280 |
+
"category": 4,
|
| 281 |
+
"input": "ذهب الى المدرسه وقابل المعلمه",
|
| 282 |
+
"runs": [
|
| 283 |
+
{
|
| 284 |
+
"run": 1,
|
| 285 |
+
"corrected": "ذهب إلى المدرسة وقابل المعلمة.",
|
| 286 |
+
"suggestions": [
|
| 287 |
+
{
|
| 288 |
+
"alternatives": [],
|
| 289 |
+
"confidence": 0.8,
|
| 290 |
+
"correction": "المعلمة.",
|
| 291 |
+
"end": 29,
|
| 292 |
+
"id": "3579ef5d-9295-46a9-8056-5a0b15dced2d",
|
| 293 |
+
"locked": true,
|
| 294 |
+
"original": "المعلمه",
|
| 295 |
+
"priority": 2,
|
| 296 |
+
"start": 22,
|
| 297 |
+
"type": "punctuation"
|
| 298 |
+
},
|
| 299 |
+
{
|
| 300 |
+
"alternatives": [],
|
| 301 |
+
"confidence": 1.0,
|
| 302 |
+
"correction": "المدرسة",
|
| 303 |
+
"end": 15,
|
| 304 |
+
"id": "af7b8dd8-f85a-4632-a7c9-b9b733d7e019",
|
| 305 |
+
"locked": true,
|
| 306 |
+
"original": "المدرسه",
|
| 307 |
+
"priority": 1,
|
| 308 |
+
"start": 8,
|
| 309 |
+
"type": "spelling"
|
| 310 |
+
},
|
| 311 |
+
{
|
| 312 |
+
"alternatives": [
|
| 313 |
+
"إلى",
|
| 314 |
+
"ال",
|
| 315 |
+
"الم",
|
| 316 |
+
"الى"
|
| 317 |
+
],
|
| 318 |
+
"confidence": 0.9,
|
| 319 |
+
"correction": "إلى",
|
| 320 |
+
"end": 7,
|
| 321 |
+
"id": "cd3a78f0-afbc-42d0-8bba-c60ce884dfdf",
|
| 322 |
+
"locked": true,
|
| 323 |
+
"original": "الى",
|
| 324 |
+
"priority": 1,
|
| 325 |
+
"start": 4,
|
| 326 |
+
"type": "spelling"
|
| 327 |
+
}
|
| 328 |
+
]
|
| 329 |
+
},
|
| 330 |
+
{
|
| 331 |
+
"run": 2,
|
| 332 |
+
"corrected": "ذهب إلى المدرسة وقابل المعلمة.",
|
| 333 |
+
"suggestions": [
|
| 334 |
+
{
|
| 335 |
+
"alternatives": [],
|
| 336 |
+
"confidence": 0.8,
|
| 337 |
+
"correction": "المعلمة.",
|
| 338 |
+
"end": 29,
|
| 339 |
+
"id": "4263a3c3-69cc-40a7-884f-a6e9bfd17eb1",
|
| 340 |
+
"locked": true,
|
| 341 |
+
"original": "المعلمه",
|
| 342 |
+
"priority": 2,
|
| 343 |
+
"start": 22,
|
| 344 |
+
"type": "punctuation"
|
| 345 |
+
},
|
| 346 |
+
{
|
| 347 |
+
"alternatives": [],
|
| 348 |
+
"confidence": 1.0,
|
| 349 |
+
"correction": "المدرسة",
|
| 350 |
+
"end": 15,
|
| 351 |
+
"id": "3c062f0a-95b6-4eee-bd80-36fc9b295206",
|
| 352 |
+
"locked": true,
|
| 353 |
+
"original": "المدرسه",
|
| 354 |
+
"priority": 1,
|
| 355 |
+
"start": 8,
|
| 356 |
+
"type": "spelling"
|
| 357 |
+
},
|
| 358 |
+
{
|
| 359 |
+
"alternatives": [
|
| 360 |
+
"إلى",
|
| 361 |
+
"ال",
|
| 362 |
+
"الم",
|
| 363 |
+
"الى"
|
| 364 |
+
],
|
| 365 |
+
"confidence": 0.9,
|
| 366 |
+
"correction": "إلى",
|
| 367 |
+
"end": 7,
|
| 368 |
+
"id": "beb1ecbe-3278-47d5-bb14-d28f1eec5b47",
|
| 369 |
+
"locked": true,
|
| 370 |
+
"original": "الى",
|
| 371 |
+
"priority": 1,
|
| 372 |
+
"start": 4,
|
| 373 |
+
"type": "spelling"
|
| 374 |
+
}
|
| 375 |
+
]
|
| 376 |
+
},
|
| 377 |
+
{
|
| 378 |
+
"run": 3,
|
| 379 |
+
"corrected": "ذهب إلى المدرسة وقابل المعلمة.",
|
| 380 |
+
"suggestions": [
|
| 381 |
+
{
|
| 382 |
+
"alternatives": [],
|
| 383 |
+
"confidence": 0.8,
|
| 384 |
+
"correction": "المعلمة.",
|
| 385 |
+
"end": 29,
|
| 386 |
+
"id": "5361ba1b-5c5f-4740-84be-1c4d96c665db",
|
| 387 |
+
"locked": true,
|
| 388 |
+
"original": "المعلمه",
|
| 389 |
+
"priority": 2,
|
| 390 |
+
"start": 22,
|
| 391 |
+
"type": "punctuation"
|
| 392 |
+
},
|
| 393 |
+
{
|
| 394 |
+
"alternatives": [],
|
| 395 |
+
"confidence": 1.0,
|
| 396 |
+
"correction": "المدرسة",
|
| 397 |
+
"end": 15,
|
| 398 |
+
"id": "f0450147-9d7a-4754-a4fe-403a07219c39",
|
| 399 |
+
"locked": true,
|
| 400 |
+
"original": "المدرسه",
|
| 401 |
+
"priority": 1,
|
| 402 |
+
"start": 8,
|
| 403 |
+
"type": "spelling"
|
| 404 |
+
},
|
| 405 |
+
{
|
| 406 |
+
"alternatives": [
|
| 407 |
+
"إلى",
|
| 408 |
+
"ال",
|
| 409 |
+
"الم",
|
| 410 |
+
"الى"
|
| 411 |
+
],
|
| 412 |
+
"confidence": 0.9,
|
| 413 |
+
"correction": "إلى",
|
| 414 |
+
"end": 7,
|
| 415 |
+
"id": "a8278394-1555-4d01-ba94-1325efc0a97c",
|
| 416 |
+
"locked": true,
|
| 417 |
+
"original": "الى",
|
| 418 |
+
"priority": 1,
|
| 419 |
+
"start": 4,
|
| 420 |
+
"type": "spelling"
|
| 421 |
+
}
|
| 422 |
+
]
|
| 423 |
+
}
|
| 424 |
+
],
|
| 425 |
+
"deterministic": true
|
| 426 |
+
},
|
| 427 |
+
{
|
| 428 |
+
"id": "C4-03",
|
| 429 |
+
"category": 4,
|
| 430 |
+
"input": "ان الطالبات ذهبو الى الجامعه",
|
| 431 |
+
"runs": [
|
| 432 |
+
{
|
| 433 |
+
"run": 1,
|
| 434 |
+
"corrected": "إن الطالبات ذهبن ذه��وا الجامعة.",
|
| 435 |
+
"suggestions": [
|
| 436 |
+
{
|
| 437 |
+
"alternatives": [],
|
| 438 |
+
"confidence": 1.0,
|
| 439 |
+
"correction": "ذهبن",
|
| 440 |
+
"end": 16,
|
| 441 |
+
"id": "bc1d01e1-8d6b-4bda-bbe0-199e841d0f3d",
|
| 442 |
+
"locked": true,
|
| 443 |
+
"original": "ذهبو",
|
| 444 |
+
"priority": 3,
|
| 445 |
+
"start": 12,
|
| 446 |
+
"type": "grammar"
|
| 447 |
+
},
|
| 448 |
+
{
|
| 449 |
+
"alternatives": [],
|
| 450 |
+
"confidence": 0.8,
|
| 451 |
+
"correction": "الجامعة.",
|
| 452 |
+
"end": 28,
|
| 453 |
+
"id": "8cdb866c-0c6f-4cb1-a4ef-d00be9b455f7",
|
| 454 |
+
"locked": true,
|
| 455 |
+
"original": "الجامعه",
|
| 456 |
+
"priority": 2,
|
| 457 |
+
"start": 21,
|
| 458 |
+
"type": "punctuation"
|
| 459 |
+
},
|
| 460 |
+
{
|
| 461 |
+
"alternatives": [],
|
| 462 |
+
"confidence": 1.0,
|
| 463 |
+
"correction": "إن",
|
| 464 |
+
"end": 2,
|
| 465 |
+
"id": "027f98a7-668c-463f-9ecc-acaad6b959b2",
|
| 466 |
+
"locked": true,
|
| 467 |
+
"original": "ان",
|
| 468 |
+
"priority": 1,
|
| 469 |
+
"start": 0,
|
| 470 |
+
"type": "spelling"
|
| 471 |
+
},
|
| 472 |
+
{
|
| 473 |
+
"alternatives": [
|
| 474 |
+
"ذهبوا",
|
| 475 |
+
"ال",
|
| 476 |
+
"الم",
|
| 477 |
+
"الى"
|
| 478 |
+
],
|
| 479 |
+
"confidence": 0.9,
|
| 480 |
+
"correction": "ذهبوا",
|
| 481 |
+
"end": 20,
|
| 482 |
+
"id": "8aee308b-6200-4c92-b6d1-95333a112ce0",
|
| 483 |
+
"locked": true,
|
| 484 |
+
"original": "الى",
|
| 485 |
+
"priority": 1,
|
| 486 |
+
"start": 17,
|
| 487 |
+
"type": "spelling"
|
| 488 |
+
}
|
| 489 |
+
]
|
| 490 |
+
},
|
| 491 |
+
{
|
| 492 |
+
"run": 2,
|
| 493 |
+
"corrected": "إن الطالبات ذهبن ذهبوا الجامعة.",
|
| 494 |
+
"suggestions": [
|
| 495 |
+
{
|
| 496 |
+
"alternatives": [],
|
| 497 |
+
"confidence": 1.0,
|
| 498 |
+
"correction": "ذهبن",
|
| 499 |
+
"end": 16,
|
| 500 |
+
"id": "0c9ec931-ea50-423c-8429-89a100e1c226",
|
| 501 |
+
"locked": true,
|
| 502 |
+
"original": "ذهبو",
|
| 503 |
+
"priority": 3,
|
| 504 |
+
"start": 12,
|
| 505 |
+
"type": "grammar"
|
| 506 |
+
},
|
| 507 |
+
{
|
| 508 |
+
"alternatives": [],
|
| 509 |
+
"confidence": 0.8,
|
| 510 |
+
"correction": "الجامعة.",
|
| 511 |
+
"end": 28,
|
| 512 |
+
"id": "c67960b7-36f0-480a-8e85-716c57465107",
|
| 513 |
+
"locked": true,
|
| 514 |
+
"original": "الجامعه",
|
| 515 |
+
"priority": 2,
|
| 516 |
+
"start": 21,
|
| 517 |
+
"type": "punctuation"
|
| 518 |
+
},
|
| 519 |
+
{
|
| 520 |
+
"alternatives": [],
|
| 521 |
+
"confidence": 1.0,
|
| 522 |
+
"correction": "إن",
|
| 523 |
+
"end": 2,
|
| 524 |
+
"id": "787d7736-29aa-4625-90ad-e1248acb2d48",
|
| 525 |
+
"locked": true,
|
| 526 |
+
"original": "ان",
|
| 527 |
+
"priority": 1,
|
| 528 |
+
"start": 0,
|
| 529 |
+
"type": "spelling"
|
| 530 |
+
},
|
| 531 |
+
{
|
| 532 |
+
"alternatives": [
|
| 533 |
+
"ذهبوا",
|
| 534 |
+
"ال",
|
| 535 |
+
"الم",
|
| 536 |
+
"الى"
|
| 537 |
+
],
|
| 538 |
+
"confidence": 0.9,
|
| 539 |
+
"correction": "ذهبوا",
|
| 540 |
+
"end": 20,
|
| 541 |
+
"id": "69c96488-d579-441c-89ea-3b66477f1f2d",
|
| 542 |
+
"locked": true,
|
| 543 |
+
"original": "الى",
|
| 544 |
+
"priority": 1,
|
| 545 |
+
"start": 17,
|
| 546 |
+
"type": "spelling"
|
| 547 |
+
}
|
| 548 |
+
]
|
| 549 |
+
},
|
| 550 |
+
{
|
| 551 |
+
"run": 3,
|
| 552 |
+
"corrected": "إن الطالبات ذهبن ذهبوا الجامعة.",
|
| 553 |
+
"suggestions": [
|
| 554 |
+
{
|
| 555 |
+
"alternatives": [],
|
| 556 |
+
"confidence": 1.0,
|
| 557 |
+
"correction": "ذهبن",
|
| 558 |
+
"end": 16,
|
| 559 |
+
"id": "e9626053-e05b-4774-bd33-2155ee6d7fba",
|
| 560 |
+
"locked": true,
|
| 561 |
+
"original": "ذهبو",
|
| 562 |
+
"priority": 3,
|
| 563 |
+
"start": 12,
|
| 564 |
+
"type": "grammar"
|
| 565 |
+
},
|
| 566 |
+
{
|
| 567 |
+
"alternatives": [],
|
| 568 |
+
"confidence": 0.8,
|
| 569 |
+
"correction": "الجامعة.",
|
| 570 |
+
"end": 28,
|
| 571 |
+
"id": "4ecab998-db9d-47b5-a835-a4516a38b1ae",
|
| 572 |
+
"locked": true,
|
| 573 |
+
"original": "الجامعه",
|
| 574 |
+
"priority": 2,
|
| 575 |
+
"start": 21,
|
| 576 |
+
"type": "punctuation"
|
| 577 |
+
},
|
| 578 |
+
{
|
| 579 |
+
"alternatives": [],
|
| 580 |
+
"confidence": 1.0,
|
| 581 |
+
"correction": "إن",
|
| 582 |
+
"end": 2,
|
| 583 |
+
"id": "864a48a7-d61a-4c9e-8953-72826c279d48",
|
| 584 |
+
"locked": true,
|
| 585 |
+
"original": "ان",
|
| 586 |
+
"priority": 1,
|
| 587 |
+
"start": 0,
|
| 588 |
+
"type": "spelling"
|
| 589 |
+
},
|
| 590 |
+
{
|
| 591 |
+
"alternatives": [
|
| 592 |
+
"ذهبوا",
|
| 593 |
+
"ال",
|
| 594 |
+
"الم",
|
| 595 |
+
"الى"
|
| 596 |
+
],
|
| 597 |
+
"confidence": 0.9,
|
| 598 |
+
"correction": "ذهبوا",
|
| 599 |
+
"end": 20,
|
| 600 |
+
"id": "05c65f7f-14f4-474c-bff8-0ce52ce5cf5b",
|
| 601 |
+
"locked": true,
|
| 602 |
+
"original": "الى",
|
| 603 |
+
"priority": 1,
|
| 604 |
+
"start": 17,
|
| 605 |
+
"type": "spelling"
|
| 606 |
+
}
|
| 607 |
+
]
|
| 608 |
+
}
|
| 609 |
+
],
|
| 610 |
+
"deterministic": true
|
| 611 |
+
}
|
| 612 |
+
],
|
| 613 |
+
"boundary_tests": [
|
| 614 |
+
{
|
| 615 |
+
"id": "BOUND-299",
|
| 616 |
+
"category": 3,
|
| 617 |
+
"input_len": 299,
|
| 618 |
+
"input": "يستخدم الذكاء الاصطناعي تقنيات التعلم العميق في معالجة البيانات يستخدم الذكاء ال...",
|
| 619 |
+
"has_spelling_suggestions": false,
|
| 620 |
+
"total_suggestions": 6,
|
| 621 |
+
"timing": {
|
| 622 |
+
"grammar_ms": 5256,
|
| 623 |
+
"punctuation_ms": 5490,
|
| 624 |
+
"spelling_ms": 32835,
|
| 625 |
+
"total_ms": 43584
|
| 626 |
+
}
|
| 627 |
+
},
|
| 628 |
+
{
|
| 629 |
+
"id": "BOUND-300",
|
| 630 |
+
"category": 3,
|
| 631 |
+
"input_len": 300,
|
| 632 |
+
"input": "يستخدم الذكاء الاصطناعي تقنيات التعلم العميق في معالجة البيانات يستخدم الذكاء ال...",
|
| 633 |
+
"has_spelling_suggestions": false,
|
| 634 |
+
"total_suggestions": 9,
|
| 635 |
+
"timing": {
|
| 636 |
+
"grammar_ms": 11035,
|
| 637 |
+
"punctuation_ms": 5849,
|
| 638 |
+
"spelling_ms": 18786,
|
| 639 |
+
"total_ms": 35674
|
| 640 |
+
}
|
| 641 |
+
},
|
| 642 |
+
{
|
| 643 |
+
"id": "BOUND-301",
|
| 644 |
+
"category": 3,
|
| 645 |
+
"input_len": 301,
|
| 646 |
+
"input": "يستخدم الذكاء الاصطناعي تقنيات التعلم العميق في معالجة البيانات يستخدم الذكاء ال...",
|
| 647 |
+
"has_spelling_suggestions": false,
|
| 648 |
+
"total_suggestions": 9,
|
| 649 |
+
"timing": {
|
| 650 |
+
"grammar_ms": 12363,
|
| 651 |
+
"punctuation_ms": 6256,
|
| 652 |
+
"spelling_ms": 3209,
|
| 653 |
+
"total_ms": 21833
|
| 654 |
+
}
|
| 655 |
+
},
|
| 656 |
+
{
|
| 657 |
+
"id": "BOUND-500",
|
| 658 |
+
"category": 3,
|
| 659 |
+
"input_len": 500,
|
| 660 |
+
"input": "يستخدم الذكاء الاصطناعي تقنيات التعلم العميق في معالجة البيانات يستخدم الذكاء ال...",
|
| 661 |
+
"has_spelling_suggestions": false,
|
| 662 |
+
"total_suggestions": 23,
|
| 663 |
+
"timing": {
|
| 664 |
+
"grammar_ms": 18635,
|
| 665 |
+
"punctuation_ms": 12917,
|
| 666 |
+
"spelling_ms": 0,
|
| 667 |
+
"total_ms": 31560
|
| 668 |
+
}
|
| 669 |
+
}
|
| 670 |
+
]
|
| 671 |
+
}
|
archive/old_tests/deep_dive_test.py
ADDED
|
@@ -0,0 +1,519 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
BAYAN Deep-Dive Test Harness — Track A (Raw Models via API) & Track B (Full Pipeline via API)
|
| 3 |
+
|
| 4 |
+
Uses the deployed HF Space API (bayan10/bayan-api) instead of loading models locally.
|
| 5 |
+
This avoids the 1GB model download hang and tests the ACTUAL production behavior.
|
| 6 |
+
|
| 7 |
+
Track A: /api/spelling, /api/grammar, /api/punctuation (individual model endpoints)
|
| 8 |
+
Track B: /api/analyze (full pipeline with StageLocker, OffsetMapper, PatchSet)
|
| 9 |
+
|
| 10 |
+
Usage:
|
| 11 |
+
python tests/deep_dive_test.py --stage spelling
|
| 12 |
+
python tests/deep_dive_test.py --stage grammar
|
| 13 |
+
python tests/deep_dive_test.py --stage punctuation
|
| 14 |
+
python tests/deep_dive_test.py --stage pipeline
|
| 15 |
+
python tests/deep_dive_test.py --stage all
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
import sys, os, re, json, time, argparse
|
| 19 |
+
from datetime import datetime, timezone
|
| 20 |
+
|
| 21 |
+
# ═══════════════════════════════════════════════════════════════════
|
| 22 |
+
# API CLIENT
|
| 23 |
+
# ═══════════════════════════════════════════════════════════════════
|
| 24 |
+
|
| 25 |
+
import requests
|
| 26 |
+
|
| 27 |
+
API_BASE = "https://bayan10-bayan-api.hf.space"
|
| 28 |
+
TIMEOUT = 60 # seconds per request
|
| 29 |
+
|
| 30 |
+
def api_call(endpoint, text, retries=2):
|
| 31 |
+
"""Call the deployed API with retry."""
|
| 32 |
+
url = f"{API_BASE}{endpoint}"
|
| 33 |
+
for attempt in range(retries + 1):
|
| 34 |
+
try:
|
| 35 |
+
t0 = time.time()
|
| 36 |
+
resp = requests.post(url, json={"text": text}, timeout=TIMEOUT)
|
| 37 |
+
elapsed = int((time.time() - t0) * 1000)
|
| 38 |
+
if resp.status_code == 200:
|
| 39 |
+
data = resp.json()
|
| 40 |
+
data['_elapsed_ms'] = elapsed
|
| 41 |
+
data['_timestamp'] = datetime.now(timezone.utc).isoformat()
|
| 42 |
+
return data
|
| 43 |
+
else:
|
| 44 |
+
if attempt < retries:
|
| 45 |
+
time.sleep(2)
|
| 46 |
+
continue
|
| 47 |
+
return {"error": f"HTTP {resp.status_code}: {resp.text[:200]}", "_elapsed_ms": elapsed}
|
| 48 |
+
except requests.exceptions.Timeout:
|
| 49 |
+
if attempt < retries:
|
| 50 |
+
time.sleep(2)
|
| 51 |
+
continue
|
| 52 |
+
return {"error": f"Timeout after {TIMEOUT}s", "_elapsed_ms": TIMEOUT * 1000}
|
| 53 |
+
except Exception as e:
|
| 54 |
+
return {"error": str(e)}
|
| 55 |
+
|
| 56 |
+
# ═══════════════════════════════════════════════════════════════════
|
| 57 |
+
# TRACK A — RAW MODEL CALLS (individual endpoints, no pipeline)
|
| 58 |
+
# ═══════════════════════════════════════════════════════════════════
|
| 59 |
+
|
| 60 |
+
def track_a_spelling(text):
|
| 61 |
+
"""Call /api/spelling — raw AraSpell output."""
|
| 62 |
+
result = api_call("/api/spelling", text)
|
| 63 |
+
if "error" in result:
|
| 64 |
+
return {"input": text, "output": text, "error": result["error"], "changed": False}
|
| 65 |
+
corrected = result.get("corrected_text", text)
|
| 66 |
+
return {
|
| 67 |
+
"input": text, "output": corrected, "changed": corrected != text,
|
| 68 |
+
"elapsed_ms": result.get("_elapsed_ms"), "timestamp": result.get("_timestamp")
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
def track_a_grammar(text):
|
| 72 |
+
"""Call /api/grammar — raw grammar model output."""
|
| 73 |
+
result = api_call("/api/grammar", text)
|
| 74 |
+
if "error" in result:
|
| 75 |
+
return {"input": text, "output": text, "error": result["error"], "changed": False}
|
| 76 |
+
corrected = result.get("corrected_text", text)
|
| 77 |
+
return {
|
| 78 |
+
"input": text, "output": corrected, "changed": corrected != text,
|
| 79 |
+
"elapsed_ms": result.get("_elapsed_ms"), "timestamp": result.get("_timestamp")
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
def track_a_punctuation(text):
|
| 83 |
+
"""Call /api/punctuation — raw PuncAra output."""
|
| 84 |
+
result = api_call("/api/punctuation", text)
|
| 85 |
+
if "error" in result:
|
| 86 |
+
return {"input": text, "output": text, "error": result["error"], "changed": False}
|
| 87 |
+
corrected = result.get("corrected_text", text)
|
| 88 |
+
marks_before = sum(1 for c in text if c in '.,;:!?،؛؟')
|
| 89 |
+
marks_after = sum(1 for c in corrected if c in '.,;:!?،؛؟')
|
| 90 |
+
return {
|
| 91 |
+
"input": text, "output": corrected, "changed": corrected != text,
|
| 92 |
+
"marks_added": marks_after - marks_before,
|
| 93 |
+
"elapsed_ms": result.get("_elapsed_ms"), "timestamp": result.get("_timestamp")
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
# ═══════════════════════════════════════════════════════════════════
|
| 97 |
+
# TRACK B — FULL PIPELINE (/api/analyze)
|
| 98 |
+
# ═══════════════════════════════════════════════════════════════════
|
| 99 |
+
|
| 100 |
+
def track_b_analyze(text):
|
| 101 |
+
"""Call /api/analyze — full pipeline with all stages."""
|
| 102 |
+
result = api_call("/api/analyze", text)
|
| 103 |
+
if "error" in result and "status" not in result:
|
| 104 |
+
return {"input": text, "error": result["error"], "suggestions": []}
|
| 105 |
+
return {
|
| 106 |
+
"input": text,
|
| 107 |
+
"original": result.get("original", text),
|
| 108 |
+
"corrected": result.get("corrected", text),
|
| 109 |
+
"suggestions": result.get("suggestions", []),
|
| 110 |
+
"timing_ms": result.get("timing_ms", {}),
|
| 111 |
+
"elapsed_ms": result.get("_elapsed_ms"),
|
| 112 |
+
"timestamp": result.get("_timestamp"),
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
# ═══════════════════════════════════════════════════════════════════
|
| 116 |
+
# TEST INPUTS — ALL CATEGORIES
|
| 117 |
+
# ═══════════════════════════════════════════════════════════════════
|
| 118 |
+
|
| 119 |
+
CAT2_OVERCORRECTION = [
|
| 120 |
+
{"id": "C2-01", "input": "القاهرة عاصمة جمهورية مصر العربية وأكبر مدنها", "domain": "news"},
|
| 121 |
+
{"id": "C2-02", "input": "يعد نهر النيل أطول أنهار العالم", "domain": "news"},
|
| 122 |
+
{"id": "C2-03", "input": "بسم الله الرحمن الرحيم", "domain": "religious"},
|
| 123 |
+
{"id": "C2-04", "input": "إنا لله وإنا إليه راجعون", "domain": "religious"},
|
| 124 |
+
{"id": "C2-05", "input": "يستخدم الذكاء الاصطناعي تقنيات التعلم العميق", "domain": "technical"},
|
| 125 |
+
{"id": "C2-06", "input": "سافر محمد إلى دبي للعمل في شركة جوجل", "domain": "proper_nouns"},
|
| 126 |
+
{"id": "C2-07", "input": "الرئيس عبد الفتاح السيسي رئيس جمهورية مصر العربية", "domain": "proper_nouns"},
|
| 127 |
+
{"id": "C2-08", "input": "استوقفني المشهد فتأملته مليا", "domain": "literary"},
|
| 128 |
+
{"id": "C2-09", "input": "أضحى التعليم الإلكتروني ضرورة ملحة في عصرنا الحالي", "domain": "formal"},
|
| 129 |
+
{"id": "C2-10", "input": "تتراوح درجات الحرارة بين خمس وعشرين وثلاثين درجة مئوية", "domain": "weather"},
|
| 130 |
+
]
|
| 131 |
+
|
| 132 |
+
CAT8_CLITIC_ROOTS = [
|
| 133 |
+
('مدرسة', 'moon'), # Moon letter
|
| 134 |
+
('شمس', 'sun'), # Sun letter
|
| 135 |
+
('أمة', 'hamza'), # Hamza-initial
|
| 136 |
+
('نافذة', 'long'), # Long word
|
| 137 |
+
('علم', 'short'), # Short 3-letter root
|
| 138 |
+
('اقتصاد', 'alef'), # Alef-initial, long
|
| 139 |
+
]
|
| 140 |
+
CAT8_PREFIXES = [("bare", ""), ("wa", "و"), ("ba", "ب"), ("la", "ل"), ("ka", "ك")]
|
| 141 |
+
CAT8_TESTS = []
|
| 142 |
+
for root, root_type in CAT8_CLITIC_ROOTS:
|
| 143 |
+
for pfx_name, pfx in CAT8_PREFIXES:
|
| 144 |
+
word = pfx + root
|
| 145 |
+
CAT8_TESTS.append({
|
| 146 |
+
"id": f"C8-{root}-{pfx_name}", "input": word, "root": root,
|
| 147 |
+
"root_type": root_type, "prefix": pfx, "expected": word,
|
| 148 |
+
})
|
| 149 |
+
|
| 150 |
+
CAT9_CONFUSABLE = [
|
| 151 |
+
# === Isolation tests ===
|
| 152 |
+
{"id": "C9-01a", "input": "ان", "context": "isolation", "concern": "should→أن/إن NOT كان"},
|
| 153 |
+
{"id": "C9-01b", "input": "كان", "context": "isolation", "concern": "stays كان"},
|
| 154 |
+
{"id": "C9-02a", "input": "إلى", "context": "isolation", "concern": "stays إلى"},
|
| 155 |
+
{"id": "C9-02b", "input": "على", "context": "isolation", "concern": "stays على"},
|
| 156 |
+
{"id": "C9-03a", "input": "هذا", "context": "isolation", "concern": "stays هذا"},
|
| 157 |
+
{"id": "C9-03b", "input": "هذه", "context": "isolation", "concern": "stays هذه"},
|
| 158 |
+
{"id": "C9-03c", "input": "هذة", "context": "isolation", "concern": "misspelling→هذه"},
|
| 159 |
+
{"id": "C9-04a", "input": "لكن", "context": "isolation", "concern": "stays لكن"},
|
| 160 |
+
{"id": "C9-04b", "input": "لاكن", "context": "isolation", "concern": "misspelling→لكن"},
|
| 161 |
+
{"id": "C9-05a", "input": "ذلك", "context": "isolation", "concern": "stays ذلك"},
|
| 162 |
+
{"id": "C9-05b", "input": "ذالك", "context": "isolation", "concern": "misspelling→ذلك"},
|
| 163 |
+
{"id": "C9-06a", "input": "الى", "context": "isolation", "concern": "should→إلى"},
|
| 164 |
+
# === Sentence-context tests ===
|
| 165 |
+
{"id": "C9-S01", "input": "ان الحياة جميلة", "context": "sentence", "concern": "ان→أن/إن NOT كان"},
|
| 166 |
+
{"id": "C9-S02", "input": "كان الرجل طيبا", "context": "sentence", "concern": "كان stays"},
|
| 167 |
+
{"id": "C9-S03", "input": "ذهب الى المدرسة", "context": "sentence", "concern": "الى→إلى"},
|
| 168 |
+
{"id": "C9-S04", "input": "جلس على الكرسي", "context": "sentence", "concern": "على stays"},
|
| 169 |
+
{"id": "C9-S05", "input": "هذة المدينة جميلة", "context": "sentence", "concern": "هذة→هذه"},
|
| 170 |
+
{"id": "C9-S06", "input": "هو ذكي لاكن كسول", "context": "sentence", "concern": "لاكن→لكن"},
|
| 171 |
+
{"id": "C9-S07", "input": "ذالك الكتاب مفيد", "context": "sentence", "concern": "ذالك→ذلك"},
|
| 172 |
+
{"id": "C9-S08", "input": "هذا البيت كبير", "context": "sentence", "concern": "هذا stays"},
|
| 173 |
+
{"id": "C9-S09", "input": "هذه السيارة سريعة", "context": "sentence", "concern": "هذه stays"},
|
| 174 |
+
{"id": "C9-S10", "input": "سافر إلى القاهرة", "context": "sentence", "concern": "إلى stays"},
|
| 175 |
+
{"id": "C9-S11", "input": "جلس على المقعد", "context": "sentence", "concern": "على stays"},
|
| 176 |
+
{"id": "C9-S12", "input": "ان الذكاء مهم لكن الاجتهاد اهم", "context": "sentence", "concern": "ان→أن, لكن stays"},
|
| 177 |
+
]
|
| 178 |
+
|
| 179 |
+
CAT10_EDGE_CASES = [
|
| 180 |
+
{"id": "C10-01", "input": "كَتَبَ الطَّالِبُ الدَّرسَ", "concern": "tashkeel_present"},
|
| 181 |
+
{"id": "C10-02", "input": "كتب الطالب الدرس", "concern": "tashkeel_absent"},
|
| 182 |
+
{"id": "C10-03", "input": "قرأ إبراهيم آيات من القرآن", "concern": "alef_forms"},
|
| 183 |
+
{"id": "C10-04", "input": "مشى الفتى إلى المستشفى", "concern": "ya_alef_maksura"},
|
| 184 |
+
{"id": "C10-05", "input": "ذهبت إلى المدرسة", "concern": "ta_marbuta"},
|
| 185 |
+
{"id": "C10-06", "input": "جاء ١٢٣ طالبا", "concern": "arabic_indic_digits"},
|
| 186 |
+
{"id": "C10-07", "input": "جاء 123 طالبا", "concern": "western_digits"},
|
| 187 |
+
{"id": "C10-08", "input": "يعمل في شركة Google في القاهرة", "concern": "latin_in_arabic"},
|
| 188 |
+
{"id": "C10-09", "input": "انا رايح المدرسة النهارده", "concern": "egyptian_dialect"},
|
| 189 |
+
{"id": "C10-10", "input": "الموضوع ده كويس جدااااا", "concern": "repeated_letters"},
|
| 190 |
+
{"id": "C10-11", "input": "مسؤول عن الشؤون الداخلية", "concern": "hamza_on_waw"},
|
| 191 |
+
{"id": "C10-12", "input": "بيئة العمل مليئة بالتحديات", "concern": "hamza_on_ya"},
|
| 192 |
+
{"id": "C10-13", "input": "الكتاب الذى قرأته مفيد", "concern": "ya_in_الذي"},
|
| 193 |
+
{"id": "C10-14", "input": "خطأ الطالب في الامتحان", "concern": "hamza_standalone"},
|
| 194 |
+
{"id": "C10-15", "input": "الحمد لله رب العالمين الرحمن الرحيم مالك يوم الدين", "concern": "religious_long"},
|
| 195 |
+
]
|
| 196 |
+
|
| 197 |
+
CAT5_PUNC_SANITY = [
|
| 198 |
+
{"id": "C5-01", "input": "ذهب إلى المدرسة", "length": "short_3w"},
|
| 199 |
+
{"id": "C5-02", "input": "هل تعلم أن الأرض تدور حول الشمس كل عام", "length": "medium_9w"},
|
| 200 |
+
{"id": "C5-03", "input": "التزم الرياضي بتناول وجباته الصحية وحساب سعراته بدقة رغبة في بناء كتلة عضلية قوية ويا له من التزام حديدي يثير الإعجاب", "length": "long_20w"},
|
| 201 |
+
{"id": "C5-04", "input": "قال المعلم للطلاب ادرسوا جيدا فالامتحان قريب", "length": "medium_imperative"},
|
| 202 |
+
{"id": "C5-05", "input": "كانت الفتيات يلعبن في الحديقة وفجأة سقطت إحداهن وبدأت تبكي بشدة", "length": "long_narrative"},
|
| 203 |
+
]
|
| 204 |
+
|
| 205 |
+
CAT6_PUNC_POSITION = [
|
| 206 |
+
{"id": "C6-01", "input": "ذهب محمد إلى المدرسة ودرس جيدا ثم عاد إلى البيت"},
|
| 207 |
+
{"id": "C6-02", "input": "إن الذكاء الاصطناعي يلعب دورا هاما لذلك يجب الاهتمام به"},
|
| 208 |
+
{"id": "C6-03", "input": "التزم الرياضي بتناول وجباته الصحية وحساب سعراته بدقة رغبة في بناء كتلة عضلية قوية ويا له من التزام حديدي يثير الإعجاب"},
|
| 209 |
+
{"id": "C6-04", "input": "كانت الفتيات يلعبن في الحديقة وفجأة سقطت إحداهن وبدأت تبكي بشدة"},
|
| 210 |
+
{"id": "C6-05", "input": "هل تعلم أن القاهرة هي عاصمة مصر وتقع على ضفاف نهر النيل"},
|
| 211 |
+
{"id": "C6-06", "input": "قال المعلم للطلاب ادرسوا جيدا فالامتحان قريب"},
|
| 212 |
+
{"id": "C6-07", "input": "يحب الأطفال اللعب في الحديقة وركوب الدراجات والجري بين الأشجار"},
|
| 213 |
+
{"id": "C6-08", "input": "رغم صعوبة الامتحان إلا أن الطلاب حققوا نتائج مبهرة"},
|
| 214 |
+
{"id": "C6-09", "input": "سافر العالم إلى عدة دول لحضور المؤتمرات العلمية ونشر أبحاثه"},
|
| 215 |
+
{"id": "C6-10", "input": "يا بني اجتهد في دراستك فالعلم نور والجهل ظلام"},
|
| 216 |
+
]
|
| 217 |
+
|
| 218 |
+
# ═══════════════════════════════════════════════════════════════════
|
| 219 |
+
# RUNNERS
|
| 220 |
+
# ═══════════════════════════════════════════════════════════════════
|
| 221 |
+
|
| 222 |
+
def log(msg):
|
| 223 |
+
print(f"[{datetime.now().strftime('%H:%M:%S')}] {msg}", flush=True)
|
| 224 |
+
|
| 225 |
+
def run_spelling_tests():
|
| 226 |
+
results = []
|
| 227 |
+
|
| 228 |
+
log("=== Category 2: Overcorrection (10 tests) ===")
|
| 229 |
+
for test in CAT2_OVERCORRECTION:
|
| 230 |
+
log(f" {test['id']}: {test['input'][:50]}...")
|
| 231 |
+
a = track_a_spelling(test['input'])
|
| 232 |
+
b = track_b_analyze(test['input'])
|
| 233 |
+
fp = a.get('changed', False)
|
| 234 |
+
result = {
|
| 235 |
+
"id": test['id'], "category": 2, "input": test['input'],
|
| 236 |
+
"domain": test['domain'],
|
| 237 |
+
"track_a_spelling": a['output'],
|
| 238 |
+
"track_a_changed": a.get('changed', False),
|
| 239 |
+
"track_b_suggestions": len(b.get('suggestions', [])),
|
| 240 |
+
"track_b_corrected": b.get('corrected', ''),
|
| 241 |
+
"is_false_positive": fp,
|
| 242 |
+
}
|
| 243 |
+
status = "⚠ FP" if fp else "✓"
|
| 244 |
+
log(f" {status} A:'{a['output'][:60]}' B_sugg:{len(b.get('suggestions',[]))}")
|
| 245 |
+
results.append(result)
|
| 246 |
+
|
| 247 |
+
log("\n=== Category 8: Clitic/Prefix (30 tests) ===")
|
| 248 |
+
for test in CAT8_TESTS:
|
| 249 |
+
a = track_a_spelling(test['input'])
|
| 250 |
+
changed = a.get('changed', False)
|
| 251 |
+
if changed:
|
| 252 |
+
# Classify: did it preserve root or mangle it?
|
| 253 |
+
output = a['output']
|
| 254 |
+
root_preserved = test['root'] in output or any(
|
| 255 |
+
test['root'][:-1] in output # partial root match
|
| 256 |
+
for _ in [1]
|
| 257 |
+
)
|
| 258 |
+
classification = "root_fixed" if root_preserved else "prefix_mangled"
|
| 259 |
+
else:
|
| 260 |
+
classification = "correct"
|
| 261 |
+
result = {
|
| 262 |
+
"id": test['id'], "category": 8, "input": test['input'],
|
| 263 |
+
"root": test['root'], "root_type": test['root_type'],
|
| 264 |
+
"prefix": test['prefix'],
|
| 265 |
+
"track_a_spelling": a['output'], "changed": changed,
|
| 266 |
+
"classification": classification,
|
| 267 |
+
}
|
| 268 |
+
if changed:
|
| 269 |
+
log(f" ⚠ {test['id']}: '{test['input']}' → '{a['output']}' [{classification}]")
|
| 270 |
+
results.append(result)
|
| 271 |
+
|
| 272 |
+
log("\n=== Category 9: Confusable Words (24 tests) ===")
|
| 273 |
+
for test in CAT9_CONFUSABLE:
|
| 274 |
+
a = track_a_spelling(test['input'])
|
| 275 |
+
result = {
|
| 276 |
+
"id": test['id'], "category": 9, "input": test['input'],
|
| 277 |
+
"context": test['context'], "concern": test['concern'],
|
| 278 |
+
"track_a_spelling": a['output'], "changed": a.get('changed', False),
|
| 279 |
+
}
|
| 280 |
+
if a.get('changed'):
|
| 281 |
+
log(f" ⚠ {test['id']}: '{test['input']}' → '{a['output']}' (concern: {test['concern']})")
|
| 282 |
+
else:
|
| 283 |
+
log(f" ✓ {test['id']}: no change")
|
| 284 |
+
results.append(result)
|
| 285 |
+
|
| 286 |
+
log("\n=== Category 10: Arabic Edge Cases (15 tests) ===")
|
| 287 |
+
for test in CAT10_EDGE_CASES:
|
| 288 |
+
a = track_a_spelling(test['input'])
|
| 289 |
+
result = {
|
| 290 |
+
"id": test['id'], "category": 10, "input": test['input'],
|
| 291 |
+
"concern": test['concern'],
|
| 292 |
+
"track_a_spelling": a['output'], "changed": a.get('changed', False),
|
| 293 |
+
}
|
| 294 |
+
if a.get('changed'):
|
| 295 |
+
log(f" ⚠ {test['id']}: '{test['input']}' → '{a['output']}' [{test['concern']}]")
|
| 296 |
+
else:
|
| 297 |
+
log(f" ✓ {test['id']}: no change [{test['concern']}]")
|
| 298 |
+
results.append(result)
|
| 299 |
+
|
| 300 |
+
return results
|
| 301 |
+
|
| 302 |
+
def run_punctuation_tests():
|
| 303 |
+
results = []
|
| 304 |
+
|
| 305 |
+
log("=== Category 5: Punctuation Sanity (5 tests) ===")
|
| 306 |
+
for test in CAT5_PUNC_SANITY:
|
| 307 |
+
log(f" {test['id']}: {test['input'][:50]}...")
|
| 308 |
+
a = track_a_punctuation(test['input'])
|
| 309 |
+
result = {
|
| 310 |
+
"id": test['id'], "category": 5, "input": test['input'],
|
| 311 |
+
"length": test['length'],
|
| 312 |
+
"track_a_punc": a['output'],
|
| 313 |
+
"marks_added": a.get('marks_added', 0),
|
| 314 |
+
"changed": a.get('changed', False),
|
| 315 |
+
}
|
| 316 |
+
log(f" Marks: +{a.get('marks_added', 0)} | Output: {a['output'][:80]}")
|
| 317 |
+
results.append(result)
|
| 318 |
+
|
| 319 |
+
log("\n=== Category 6: Punctuation Position (10 tests) ===")
|
| 320 |
+
for test in CAT6_PUNC_POSITION:
|
| 321 |
+
log(f" {test['id']}: {test['input'][:50]}...")
|
| 322 |
+
# Track A: raw punctuation on original text
|
| 323 |
+
a_punc = track_a_punctuation(test['input'])
|
| 324 |
+
# Track B: full pipeline
|
| 325 |
+
b = track_b_analyze(test['input'])
|
| 326 |
+
|
| 327 |
+
# Measure: where did Track A put punctuation marks?
|
| 328 |
+
a_marks = _find_punct_positions(test['input'], a_punc['output'])
|
| 329 |
+
# Measure: where did Track B put punctuation suggestions?
|
| 330 |
+
b_punc_sugg = [s for s in b.get('suggestions', []) if s.get('type') == 'punctuation']
|
| 331 |
+
b_marks = [(s.get('start', 0), s.get('end', 0), s.get('correction', '')) for s in b_punc_sugg]
|
| 332 |
+
|
| 333 |
+
result = {
|
| 334 |
+
"id": test['id'], "category": 6, "input": test['input'],
|
| 335 |
+
"track_a_punc_output": a_punc['output'],
|
| 336 |
+
"track_a_marks": a_marks,
|
| 337 |
+
"track_b_corrected": b.get('corrected', ''),
|
| 338 |
+
"track_b_punc_suggestions": b_punc_sugg,
|
| 339 |
+
"track_b_marks": b_marks,
|
| 340 |
+
}
|
| 341 |
+
log(f" A marks: {a_marks}")
|
| 342 |
+
log(f" B marks: {b_marks}")
|
| 343 |
+
results.append(result)
|
| 344 |
+
|
| 345 |
+
return results
|
| 346 |
+
|
| 347 |
+
def _find_punct_positions(original, punctuated):
|
| 348 |
+
"""Find where punctuation was added by comparing original vs punctuated."""
|
| 349 |
+
PUNC = set('.,;:!?،؛؟')
|
| 350 |
+
marks = []
|
| 351 |
+
# Word-level alignment
|
| 352 |
+
orig_words = original.split()
|
| 353 |
+
punc_words = punctuated.split()
|
| 354 |
+
oi, pi = 0, 0
|
| 355 |
+
char_pos = 0
|
| 356 |
+
while oi < len(orig_words) and pi < len(punc_words):
|
| 357 |
+
o_base = ''.join(c for c in orig_words[oi] if c not in PUNC)
|
| 358 |
+
p_base = ''.join(c for c in punc_words[pi] if c not in PUNC)
|
| 359 |
+
if o_base == p_base:
|
| 360 |
+
# Same word — check for added punctuation
|
| 361 |
+
o_punc = set(c for c in orig_words[oi] if c in PUNC)
|
| 362 |
+
p_punc = set(c for c in punc_words[pi] if c in PUNC)
|
| 363 |
+
added = p_punc - o_punc
|
| 364 |
+
if added:
|
| 365 |
+
marks.append({
|
| 366 |
+
"word_index": oi, "word": orig_words[oi],
|
| 367 |
+
"after_word": orig_words[oi],
|
| 368 |
+
"marks_added": list(added),
|
| 369 |
+
"char_pos": char_pos,
|
| 370 |
+
})
|
| 371 |
+
char_pos += len(orig_words[oi]) + 1 # +1 for space
|
| 372 |
+
oi += 1
|
| 373 |
+
pi += 1
|
| 374 |
+
else:
|
| 375 |
+
# Mismatch — model changed the word
|
| 376 |
+
char_pos += len(orig_words[oi]) + 1
|
| 377 |
+
oi += 1
|
| 378 |
+
pi += 1
|
| 379 |
+
return marks
|
| 380 |
+
|
| 381 |
+
def run_pipeline_comparison():
|
| 382 |
+
"""Run tests that need both Track A and Track B for comparison (Cat 1, 3, 4, 7)."""
|
| 383 |
+
results = []
|
| 384 |
+
|
| 385 |
+
# Cat 3: Integration-only — test where raw models work but pipeline might not
|
| 386 |
+
log("=== Category 3: Integration-Only (5 tests) ===")
|
| 387 |
+
integration_inputs = [
|
| 388 |
+
{"id": "C3-01", "input": "كانت الفتيات يلعبون في الحديقه وفجأه سقطت احداهن وبدءت تبكي بشده"},
|
| 389 |
+
{"id": "C3-02", "input": "ان الذكاء الاصطناعي يلعب دورا هاما ولذالك يجب الاهتمام بة"},
|
| 390 |
+
{"id": "C3-03", "input": "التزم الرياضي بتناول وجباته الصحية وحساب سعراته بدقة رغبة في بناء كتلة عضلية قوية ويا له من التزام حديدي يثير الإعجاب"},
|
| 391 |
+
{"id": "C3-04", "input": "هذة المدينه جميله جدا ومناخها معتدل طوال العام"},
|
| 392 |
+
{"id": "C3-05", "input": "الطلاب الذين اجتهدو في دراستهم حققو نتائج ممتازه في الامتحانات"},
|
| 393 |
+
]
|
| 394 |
+
for test in integration_inputs:
|
| 395 |
+
log(f" {test['id']}: {test['input'][:50]}...")
|
| 396 |
+
a_spell = track_a_spelling(test['input'])
|
| 397 |
+
a_gram = track_a_grammar(test['input'])
|
| 398 |
+
a_punc = track_a_punctuation(test['input'])
|
| 399 |
+
b = track_b_analyze(test['input'])
|
| 400 |
+
result = {
|
| 401 |
+
"id": test['id'], "category": 3, "input": test['input'],
|
| 402 |
+
"track_a": {
|
| 403 |
+
"spelling": a_spell['output'], "spelling_changed": a_spell.get('changed'),
|
| 404 |
+
"grammar": a_gram['output'], "grammar_changed": a_gram.get('changed'),
|
| 405 |
+
"punctuation": a_punc['output'], "punctuation_changed": a_punc.get('changed'),
|
| 406 |
+
},
|
| 407 |
+
"track_b": {
|
| 408 |
+
"corrected": b.get('corrected', ''),
|
| 409 |
+
"suggestions": b.get('suggestions', []),
|
| 410 |
+
"timing_ms": b.get('timing_ms', {}),
|
| 411 |
+
}
|
| 412 |
+
}
|
| 413 |
+
log(f" A_spell: {a_spell['output'][:60]}")
|
| 414 |
+
log(f" A_gram: {a_gram['output'][:60]}")
|
| 415 |
+
log(f" A_punc: {a_punc['output'][:60]}")
|
| 416 |
+
log(f" B_final: {b.get('corrected','')[:60]}")
|
| 417 |
+
log(f" B_sugg: {len(b.get('suggestions',[]))}")
|
| 418 |
+
results.append(result)
|
| 419 |
+
|
| 420 |
+
# Cat 4: Overlap — run 3x for determinism
|
| 421 |
+
log("\n=== Category 4: Overlap Resolution (3 tests × 3 runs) ===")
|
| 422 |
+
overlap_inputs = [
|
| 423 |
+
{"id": "C4-01", "input": "كانت الفتيات يلعبون في الحديقه"},
|
| 424 |
+
{"id": "C4-02", "input": "ذهب الى المدرسه وقابل المعلمه"},
|
| 425 |
+
{"id": "C4-03", "input": "ان الطالبات ذهبو الى الجامعه"},
|
| 426 |
+
]
|
| 427 |
+
for test in overlap_inputs:
|
| 428 |
+
runs = []
|
| 429 |
+
for run_idx in range(3):
|
| 430 |
+
b = track_b_analyze(test['input'])
|
| 431 |
+
runs.append({
|
| 432 |
+
"run": run_idx + 1,
|
| 433 |
+
"corrected": b.get('corrected', ''),
|
| 434 |
+
"suggestions": b.get('suggestions', []),
|
| 435 |
+
})
|
| 436 |
+
# Check determinism
|
| 437 |
+
all_same = all(r['corrected'] == runs[0]['corrected'] for r in runs)
|
| 438 |
+
result = {
|
| 439 |
+
"id": test['id'], "category": 4, "input": test['input'],
|
| 440 |
+
"runs": runs, "deterministic": all_same,
|
| 441 |
+
}
|
| 442 |
+
log(f" {test['id']}: deterministic={all_same}")
|
| 443 |
+
for r in runs:
|
| 444 |
+
log(f" Run {r['run']}: {r['corrected'][:60]} ({len(r['suggestions'])} sugg)")
|
| 445 |
+
results.append(result)
|
| 446 |
+
|
| 447 |
+
return results
|
| 448 |
+
|
| 449 |
+
# Boundary tests for spelling 300-char cutoff
|
| 450 |
+
def run_boundary_tests():
|
| 451 |
+
results = []
|
| 452 |
+
log("\n=== Boundary: Spelling 300-char cutoff ===")
|
| 453 |
+
base = "يستخدم الذكاء الاصطناعي تقنيات التعلم العميق في معالجة البيانات "
|
| 454 |
+
for target_len in [299, 300, 301, 500]:
|
| 455 |
+
text = (base * 10)[:target_len]
|
| 456 |
+
b = track_b_analyze(text)
|
| 457 |
+
has_spelling = any(s.get('type') == 'spelling' for s in b.get('suggestions', []))
|
| 458 |
+
result = {
|
| 459 |
+
"id": f"BOUND-{target_len}", "category": 3, "input_len": target_len,
|
| 460 |
+
"input": text[:80] + "...",
|
| 461 |
+
"has_spelling_suggestions": has_spelling,
|
| 462 |
+
"total_suggestions": len(b.get('suggestions', [])),
|
| 463 |
+
"timing": b.get('timing_ms', {}),
|
| 464 |
+
}
|
| 465 |
+
log(f" len={target_len}: spelling_active={has_spelling} suggestions={len(b.get('suggestions',[]))}")
|
| 466 |
+
results.append(result)
|
| 467 |
+
return results
|
| 468 |
+
|
| 469 |
+
# ═══════════════════════════════════════════════════════════════════
|
| 470 |
+
# MAIN
|
| 471 |
+
# ═══════════════════════════════════════════════════════════════════
|
| 472 |
+
|
| 473 |
+
def main():
|
| 474 |
+
parser = argparse.ArgumentParser(description='BAYAN Deep-Dive Test Harness')
|
| 475 |
+
parser.add_argument('--stage', choices=['spelling', 'grammar', 'punctuation', 'pipeline', 'all'],
|
| 476 |
+
default='spelling')
|
| 477 |
+
args = parser.parse_args()
|
| 478 |
+
|
| 479 |
+
all_results = {"timestamp": datetime.now(timezone.utc).isoformat(), "api_base": API_BASE}
|
| 480 |
+
|
| 481 |
+
# Health check
|
| 482 |
+
log(f"Checking API health at {API_BASE}...")
|
| 483 |
+
try:
|
| 484 |
+
resp = requests.get(f"{API_BASE}/api/health", timeout=10)
|
| 485 |
+
log(f" Health: {resp.status_code} — {resp.json()}")
|
| 486 |
+
all_results['health'] = resp.json()
|
| 487 |
+
except Exception as e:
|
| 488 |
+
log(f" ⚠ API unreachable: {e}")
|
| 489 |
+
all_results['health'] = {"error": str(e)}
|
| 490 |
+
|
| 491 |
+
if args.stage in ('spelling', 'all'):
|
| 492 |
+
log("\n══════ SPELLING TESTS (Cat 2, 8, 9, 10) ══════")
|
| 493 |
+
all_results['spelling_tests'] = run_spelling_tests()
|
| 494 |
+
|
| 495 |
+
if args.stage in ('punctuation', 'all'):
|
| 496 |
+
log("\n══════ PUNCTUATION TESTS (Cat 5, 6) ══════")
|
| 497 |
+
all_results['punctuation_tests'] = run_punctuation_tests()
|
| 498 |
+
|
| 499 |
+
if args.stage in ('pipeline', 'all'):
|
| 500 |
+
log("\n══════ PIPELINE TESTS (Cat 3, 4) ══════")
|
| 501 |
+
all_results['pipeline_tests'] = run_pipeline_comparison()
|
| 502 |
+
all_results['boundary_tests'] = run_boundary_tests()
|
| 503 |
+
|
| 504 |
+
# Save
|
| 505 |
+
output_path = os.path.join(os.path.dirname(__file__), 'deep_dive_output.json')
|
| 506 |
+
with open(output_path, 'w', encoding='utf-8') as f:
|
| 507 |
+
json.dump(all_results, f, ensure_ascii=False, indent=2)
|
| 508 |
+
log(f"\nResults saved to {output_path}")
|
| 509 |
+
|
| 510 |
+
# Summary
|
| 511 |
+
for key in ['spelling_tests', 'punctuation_tests', 'pipeline_tests', 'boundary_tests']:
|
| 512 |
+
if key in all_results:
|
| 513 |
+
tests = all_results[key]
|
| 514 |
+
if isinstance(tests, list):
|
| 515 |
+
changed = sum(1 for t in tests if t.get('changed') or t.get('is_false_positive'))
|
| 516 |
+
log(f" {key}: {len(tests)} tests, {changed} with changes")
|
| 517 |
+
|
| 518 |
+
if __name__ == '__main__':
|
| 519 |
+
main()
|
archive/old_tests/gap_filling_results.json
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"phase_1_3": [
|
| 3 |
+
{
|
| 4 |
+
"input": "لكن الأمر مختلف",
|
| 5 |
+
"corrected": "لكن الأمر مختلف.",
|
| 6 |
+
"check": "لكن",
|
| 7 |
+
"status": "✅ PRESERVED",
|
| 8 |
+
"suggestions": 1
|
| 9 |
+
},
|
| 10 |
+
{
|
| 11 |
+
"input": "ذلك الكتاب جميل",
|
| 12 |
+
"corrected": "ذلك الكتاب جميل.",
|
| 13 |
+
"check": "ذلك",
|
| 14 |
+
"status": "✅ PRESERVED",
|
| 15 |
+
"suggestions": 1
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
"input": "إلى المدرسة",
|
| 19 |
+
"corrected": "إلى المدرسة.",
|
| 20 |
+
"check": "إلى",
|
| 21 |
+
"status": "✅ PRESERVED",
|
| 22 |
+
"suggestions": 1
|
| 23 |
+
},
|
| 24 |
+
{
|
| 25 |
+
"input": "على الطاولة",
|
| 26 |
+
"corrected": "على الطاولة.",
|
| 27 |
+
"check": "على",
|
| 28 |
+
"status": "✅ PRESERVED",
|
| 29 |
+
"suggestions": 1
|
| 30 |
+
},
|
| 31 |
+
{
|
| 32 |
+
"input": "هذه المدينة جميلة",
|
| 33 |
+
"corrected": "هذه المدينة جميلة.",
|
| 34 |
+
"check": "هذه",
|
| 35 |
+
"status": "✅ PRESERVED",
|
| 36 |
+
"suggestions": 1
|
| 37 |
+
},
|
| 38 |
+
{
|
| 39 |
+
"input": "كان الجو حارا",
|
| 40 |
+
"corrected": "كان الجو حارا.",
|
| 41 |
+
"check": "كان",
|
| 42 |
+
"status": "✅ PRESERVED",
|
| 43 |
+
"suggestions": 1
|
| 44 |
+
},
|
| 45 |
+
{
|
| 46 |
+
"input": "لاكن الأمر مختلف",
|
| 47 |
+
"corrected": "لكن الأمر مختلف.",
|
| 48 |
+
"check": "لاكن→لكن",
|
| 49 |
+
"status": "✅ CORRECTED",
|
| 50 |
+
"suggestions": 2
|
| 51 |
+
},
|
| 52 |
+
{
|
| 53 |
+
"input": "ذالك الكتاب جميل",
|
| 54 |
+
"corrected": "ذلك الكتاب جميل.",
|
| 55 |
+
"check": "ذالك→ذلك",
|
| 56 |
+
"status": "✅ CORRECTED",
|
| 57 |
+
"suggestions": 2
|
| 58 |
+
}
|
| 59 |
+
],
|
| 60 |
+
"phase_2": {
|
| 61 |
+
"total": 10,
|
| 62 |
+
"raw_fp_count": 5,
|
| 63 |
+
"raw_fp_rate": "50%",
|
| 64 |
+
"pipeline_fp_count": 1,
|
| 65 |
+
"pipeline_fp_rate": "10%",
|
| 66 |
+
"results": [
|
| 67 |
+
{
|
| 68 |
+
"id": "R-01",
|
| 69 |
+
"word": "عصماء",
|
| 70 |
+
"raw_changed": true,
|
| 71 |
+
"pipeline_changed": false,
|
| 72 |
+
"pipeline_targeted": false,
|
| 73 |
+
"is_false_positive": false
|
| 74 |
+
},
|
| 75 |
+
{
|
| 76 |
+
"id": "R-02",
|
| 77 |
+
"word": "يستشف",
|
| 78 |
+
"raw_changed": true,
|
| 79 |
+
"pipeline_changed": false,
|
| 80 |
+
"pipeline_targeted": false,
|
| 81 |
+
"is_false_positive": false
|
| 82 |
+
},
|
| 83 |
+
{
|
| 84 |
+
"id": "R-03",
|
| 85 |
+
"word": "المسغبة",
|
| 86 |
+
"raw_changed": true,
|
| 87 |
+
"pipeline_changed": false,
|
| 88 |
+
"pipeline_targeted": false,
|
| 89 |
+
"is_false_positive": false
|
| 90 |
+
},
|
| 91 |
+
{
|
| 92 |
+
"id": "R-04",
|
| 93 |
+
"word": "التقاعس",
|
| 94 |
+
"raw_changed": true,
|
| 95 |
+
"pipeline_changed": false,
|
| 96 |
+
"pipeline_targeted": false,
|
| 97 |
+
"is_false_positive": false
|
| 98 |
+
},
|
| 99 |
+
{
|
| 100 |
+
"id": "R-05",
|
| 101 |
+
"word": "استئثار",
|
| 102 |
+
"raw_changed": false,
|
| 103 |
+
"pipeline_changed": false,
|
| 104 |
+
"pipeline_targeted": false,
|
| 105 |
+
"is_false_positive": false
|
| 106 |
+
},
|
| 107 |
+
{
|
| 108 |
+
"id": "R-06",
|
| 109 |
+
"word": "تبجيل",
|
| 110 |
+
"raw_changed": false,
|
| 111 |
+
"pipeline_changed": false,
|
| 112 |
+
"pipeline_targeted": false,
|
| 113 |
+
"is_false_positive": false
|
| 114 |
+
},
|
| 115 |
+
{
|
| 116 |
+
"id": "R-07",
|
| 117 |
+
"word": "الدمث",
|
| 118 |
+
"raw_changed": true,
|
| 119 |
+
"pipeline_changed": true,
|
| 120 |
+
"pipeline_targeted": true,
|
| 121 |
+
"is_false_positive": true
|
| 122 |
+
},
|
| 123 |
+
{
|
| 124 |
+
"id": "R-08",
|
| 125 |
+
"word": "استقصاء",
|
| 126 |
+
"raw_changed": false,
|
| 127 |
+
"pipeline_changed": false,
|
| 128 |
+
"pipeline_targeted": false,
|
| 129 |
+
"is_false_positive": false
|
| 130 |
+
},
|
| 131 |
+
{
|
| 132 |
+
"id": "R-09",
|
| 133 |
+
"word": "التواني",
|
| 134 |
+
"raw_changed": false,
|
| 135 |
+
"pipeline_changed": false,
|
| 136 |
+
"pipeline_targeted": false,
|
| 137 |
+
"is_false_positive": false
|
| 138 |
+
},
|
| 139 |
+
{
|
| 140 |
+
"id": "R-10",
|
| 141 |
+
"word": "مستطرف",
|
| 142 |
+
"raw_changed": false,
|
| 143 |
+
"pipeline_changed": false,
|
| 144 |
+
"pipeline_targeted": false,
|
| 145 |
+
"is_false_positive": false
|
| 146 |
+
}
|
| 147 |
+
]
|
| 148 |
+
},
|
| 149 |
+
"phase_3_2": [
|
| 150 |
+
{
|
| 151 |
+
"input": "ولذالك قررت السفر",
|
| 152 |
+
"corrected": "ولذالك قررت السفر.",
|
| 153 |
+
"bad_split_present": false,
|
| 154 |
+
"good_correction_present": false
|
| 155 |
+
},
|
| 156 |
+
{
|
| 157 |
+
"input": "المستشفياتهم كبيرة",
|
| 158 |
+
"corrected": "المستشفيات هم كبيرة.",
|
| 159 |
+
"bad_split_present": false,
|
| 160 |
+
"good_correction_present": false
|
| 161 |
+
}
|
| 162 |
+
],
|
| 163 |
+
"phase_5_5": [
|
| 164 |
+
{
|
| 165 |
+
"input": "الطالبه كتبو الوجبات",
|
| 166 |
+
"corrected": "الطالبة كتبو الوجبات.",
|
| 167 |
+
"suggestions": 2,
|
| 168 |
+
"has_duplicate": false,
|
| 169 |
+
"word_count_diff": 0
|
| 170 |
+
},
|
| 171 |
+
{
|
| 172 |
+
"input": "هو ذهبو الي البيت",
|
| 173 |
+
"corrected": "هو ذهب إلى البيت.",
|
| 174 |
+
"suggestions": 3,
|
| 175 |
+
"has_duplicate": false,
|
| 176 |
+
"word_count_diff": 0
|
| 177 |
+
},
|
| 178 |
+
{
|
| 179 |
+
"input": "الطلاب اجتهدو في امتحانتهم",
|
| 180 |
+
"corrected": "الطلاب اجتهدو في امتحانتهم.",
|
| 181 |
+
"suggestions": 1,
|
| 182 |
+
"has_duplicate": false,
|
| 183 |
+
"word_count_diff": 0
|
| 184 |
+
}
|
| 185 |
+
],
|
| 186 |
+
"phase_6_3": {
|
| 187 |
+
"empty_count": 0,
|
| 188 |
+
"error_count": 0,
|
| 189 |
+
"results": [
|
| 190 |
+
{
|
| 191 |
+
"attempt": 1,
|
| 192 |
+
"corrected": "الحديقة جميلة والأزهار متفتحة.",
|
| 193 |
+
"suggestions": 2,
|
| 194 |
+
"status": "success",
|
| 195 |
+
"warnings": {},
|
| 196 |
+
"is_empty": false,
|
| 197 |
+
"is_error": false
|
| 198 |
+
},
|
| 199 |
+
{
|
| 200 |
+
"attempt": 2,
|
| 201 |
+
"corrected": "الحديقة جميلة والأزهار متفتحة.",
|
| 202 |
+
"suggestions": 2,
|
| 203 |
+
"status": "success",
|
| 204 |
+
"warnings": {},
|
| 205 |
+
"is_empty": false,
|
| 206 |
+
"is_error": false
|
| 207 |
+
},
|
| 208 |
+
{
|
| 209 |
+
"attempt": 3,
|
| 210 |
+
"corrected": "الحديقة جميلة والأزهار متفتحة.",
|
| 211 |
+
"suggestions": 2,
|
| 212 |
+
"status": "success",
|
| 213 |
+
"warnings": {},
|
| 214 |
+
"is_empty": false,
|
| 215 |
+
"is_error": false
|
| 216 |
+
},
|
| 217 |
+
{
|
| 218 |
+
"attempt": 4,
|
| 219 |
+
"corrected": "الحديقة جميلة والأزهار متفتحة.",
|
| 220 |
+
"suggestions": 2,
|
| 221 |
+
"status": "success",
|
| 222 |
+
"warnings": {},
|
| 223 |
+
"is_empty": false,
|
| 224 |
+
"is_error": false
|
| 225 |
+
},
|
| 226 |
+
{
|
| 227 |
+
"attempt": 5,
|
| 228 |
+
"corrected": "الحديقة جميلة والأزهار متفتحة.",
|
| 229 |
+
"suggestions": 2,
|
| 230 |
+
"status": "success",
|
| 231 |
+
"warnings": {},
|
| 232 |
+
"is_empty": false,
|
| 233 |
+
"is_error": false
|
| 234 |
+
}
|
| 235 |
+
]
|
| 236 |
+
},
|
| 237 |
+
"phase_6_4": {
|
| 238 |
+
"input_chars": 982,
|
| 239 |
+
"input_words": 159,
|
| 240 |
+
"status": "success",
|
| 241 |
+
"suggestions": 4,
|
| 242 |
+
"warnings": {},
|
| 243 |
+
"timing": {
|
| 244 |
+
"grammar_ms": 12196,
|
| 245 |
+
"punctuation_ms": 14448,
|
| 246 |
+
"spelling_ms": 0,
|
| 247 |
+
"total_ms": 26649
|
| 248 |
+
},
|
| 249 |
+
"elapsed_ms": 27615,
|
| 250 |
+
"is_silently_empty": false
|
| 251 |
+
},
|
| 252 |
+
"phase_7_1": {
|
| 253 |
+
"input": "قال المعلم للطلاب ادرسوا جيدا فالامتحان قريب",
|
| 254 |
+
"raw_output": "قال المعلم للطلاب ادرسوا: جيدا فالامتحان قريب؛",
|
| 255 |
+
"pipeline_output": "قال المعلم للطلاب ادرسوا: جيدا فالامتحين قريب",
|
| 256 |
+
"has_semicolon_raw": true,
|
| 257 |
+
"has_semicolon_pipeline": false,
|
| 258 |
+
"diagnosis": "StageLocker or validate_punctuation_diff rejection",
|
| 259 |
+
"pipeline_punc_count": 1
|
| 260 |
+
}
|
| 261 |
+
}
|
archive/old_tests/gap_filling_tests.py
ADDED
|
@@ -0,0 +1,522 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Gap-filling live tests for all missing items from the Fix-Everything prompt.
|
| 3 |
+
Covers:
|
| 4 |
+
Phase 1.3 — Category 9 pairs: لكن/لاكن, ذلك/ذالك, الى/إلى live verification
|
| 5 |
+
Phase 2 — R-01→R-10 rare vocabulary FP measurement
|
| 6 |
+
Phase 3.2 — ولذالك and مستشفياتهم specific cases
|
| 7 |
+
Phase 5.5 — Constructed dual-correction cases
|
| 8 |
+
Phase 6.3 — BUG-017 re-test
|
| 9 |
+
Phase 6.4 — 187-word input regression
|
| 10 |
+
Phase 7.1 — BUG-018 precise tracing
|
| 11 |
+
"""
|
| 12 |
+
import sys, os, json, time, requests
|
| 13 |
+
|
| 14 |
+
API_BASE = "https://bayan10-bayan-api.hf.space"
|
| 15 |
+
TIMEOUT = 90
|
| 16 |
+
|
| 17 |
+
def api_call(endpoint, text, timeout=TIMEOUT):
|
| 18 |
+
url = f"{API_BASE}{endpoint}"
|
| 19 |
+
try:
|
| 20 |
+
t0 = time.time()
|
| 21 |
+
resp = requests.post(url, json={"text": text}, timeout=timeout)
|
| 22 |
+
elapsed = int((time.time() - t0) * 1000)
|
| 23 |
+
if resp.status_code == 200:
|
| 24 |
+
data = resp.json()
|
| 25 |
+
data['_elapsed_ms'] = elapsed
|
| 26 |
+
return data
|
| 27 |
+
return {"error": f"HTTP {resp.status_code}", "_elapsed_ms": elapsed}
|
| 28 |
+
except Exception as e:
|
| 29 |
+
return {"error": f"{type(e).__name__}: {e}"}
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
all_results = {}
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
# ══════════════════════════════════════════════════════════════════════
|
| 36 |
+
# Phase 1.3 — Category 9 Pairs Live Verification
|
| 37 |
+
# ══════════════════════════════════════════════════════════════════════
|
| 38 |
+
def test_category9_live():
|
| 39 |
+
print("=" * 70)
|
| 40 |
+
print("PHASE 1.3 — Category 9 Pairs Live Verification")
|
| 41 |
+
print("=" * 70)
|
| 42 |
+
|
| 43 |
+
pairs = [
|
| 44 |
+
# (input_text, word_that_must_NOT_change, description)
|
| 45 |
+
("لكن الأمر مختلف", "لكن", "لكن must NOT become لاكن"),
|
| 46 |
+
("ذلك الكتاب جميل", "ذلك", "ذلك must NOT become ذالك"),
|
| 47 |
+
("إلى المدرسة", "إلى", "إلى must NOT become على"),
|
| 48 |
+
("على الطاولة", "على", "على must NOT become إلى"),
|
| 49 |
+
("هذه المدينة جميلة", "هذه", "هذه must NOT become هذة"),
|
| 50 |
+
("كان الجو حارا", "كان", "كان must NOT become كأن"),
|
| 51 |
+
# Reverse direction: misspellings SHOULD be corrected
|
| 52 |
+
("لاكن الأمر مختلف", "لاكن→لكن", "لاكن should become لكن"),
|
| 53 |
+
("ذالك الكتاب جميل", "ذالك→ذلك", "ذالك should become ذلك"),
|
| 54 |
+
]
|
| 55 |
+
|
| 56 |
+
results = []
|
| 57 |
+
for text, check, desc in pairs:
|
| 58 |
+
r = api_call("/api/analyze", text)
|
| 59 |
+
corrected = r.get("corrected", text)
|
| 60 |
+
suggestions = r.get("suggestions", [])
|
| 61 |
+
|
| 62 |
+
is_reverse = "→" in check
|
| 63 |
+
if is_reverse:
|
| 64 |
+
# For misspellings, check that correction happened
|
| 65 |
+
orig, expected = check.split("→")
|
| 66 |
+
if expected in corrected and orig not in corrected:
|
| 67 |
+
status = "✅ CORRECTED"
|
| 68 |
+
elif orig in corrected:
|
| 69 |
+
status = "⚠ NOT corrected (pipeline didn't fix misspelling)"
|
| 70 |
+
else:
|
| 71 |
+
status = "⚠ UNCLEAR"
|
| 72 |
+
else:
|
| 73 |
+
# For correct words, check they weren't corrupted
|
| 74 |
+
if check in corrected:
|
| 75 |
+
status = "✅ PRESERVED"
|
| 76 |
+
else:
|
| 77 |
+
status = "❌ CORRUPTED"
|
| 78 |
+
|
| 79 |
+
result = {
|
| 80 |
+
"input": text, "corrected": corrected,
|
| 81 |
+
"check": check, "status": status,
|
| 82 |
+
"suggestions": len(suggestions),
|
| 83 |
+
}
|
| 84 |
+
results.append(result)
|
| 85 |
+
print(f"\n {desc}")
|
| 86 |
+
print(f" Input: '{text}'")
|
| 87 |
+
print(f" Corrected: '{corrected}'")
|
| 88 |
+
print(f" {status}")
|
| 89 |
+
|
| 90 |
+
return results
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
# ══════════════════════════════════════════════════════════════════════
|
| 94 |
+
# Phase 2 — R-01→R-10 Rare Vocabulary FP Measurement
|
| 95 |
+
# ══════════════════════════════════════════════════════════════════════
|
| 96 |
+
def test_rare_vocabulary():
|
| 97 |
+
print("\n" + "=" * 70)
|
| 98 |
+
print("PHASE 2 — R-01→R-10 Rare Vocabulary FP Measurement")
|
| 99 |
+
print("=" * 70)
|
| 100 |
+
|
| 101 |
+
# R-01 through R-10: valid but uncommon Arabic words
|
| 102 |
+
rare_words = [
|
| 103 |
+
{"id": "R-01", "word": "عصماء", "sentence": "المدينة العصماء تحتضن آلاف السكان",
|
| 104 |
+
"desc": "عصماء = impeccable (feminine)"},
|
| 105 |
+
{"id": "R-02", "word": "يستشف", "sentence": "يستشف الباحث نتائج الدراسة بعناية",
|
| 106 |
+
"desc": "يستشف = to discern/perceive"},
|
| 107 |
+
{"id": "R-03", "word": "المسغبة", "sentence": "أرهقته المسغبة والعطش الشديد",
|
| 108 |
+
"desc": "المسغبة = severe hunger"},
|
| 109 |
+
{"id": "R-04", "word": "التقاعس", "sentence": "التقاعس عن العمل يؤدي إلى الفشل",
|
| 110 |
+
"desc": "التقاعس = negligence/laziness"},
|
| 111 |
+
{"id": "R-05", "word": "استئثار", "sentence": "استئثار السلطة يهدد الديمقراطية",
|
| 112 |
+
"desc": "استئثار = monopolization"},
|
| 113 |
+
{"id": "R-06", "word": "تبجيل", "sentence": "تبجيل العلماء واجب على المجتمع",
|
| 114 |
+
"desc": "تبجيل = veneration"},
|
| 115 |
+
{"id": "R-07", "word": "الدمث", "sentence": "الرجل الدمث يحبه الجميع",
|
| 116 |
+
"desc": "الدمث = gentle/affable person"},
|
| 117 |
+
{"id": "R-08", "word": "استقصاء", "sentence": "استقصاء الحقائق مهم في الصحافة",
|
| 118 |
+
"desc": "استقصاء = investigation/inquiry"},
|
| 119 |
+
{"id": "R-09", "word": "التواني", "sentence": "لا يجوز التواني في طلب العلم",
|
| 120 |
+
"desc": "التواني = procrastination"},
|
| 121 |
+
{"id": "R-10", "word": "مستطرف", "sentence": "كتاب المستطرف من أمهات الكتب العربية",
|
| 122 |
+
"desc": "مستطرف = novel/curious (literary term)"},
|
| 123 |
+
]
|
| 124 |
+
|
| 125 |
+
false_positives = 0
|
| 126 |
+
total = len(rare_words)
|
| 127 |
+
results = []
|
| 128 |
+
|
| 129 |
+
for item in rare_words:
|
| 130 |
+
# Track A: Raw spelling
|
| 131 |
+
a = api_call("/api/spelling", item["sentence"])
|
| 132 |
+
a_out = a.get("corrected_text", item["sentence"])
|
| 133 |
+
a_changed_word = item["word"] not in a_out
|
| 134 |
+
|
| 135 |
+
# Track B: Pipeline
|
| 136 |
+
b = api_call("/api/analyze", item["sentence"])
|
| 137 |
+
b_out = b.get("corrected", item["sentence"])
|
| 138 |
+
b_suggestions = b.get("suggestions", [])
|
| 139 |
+
b_changed_word = item["word"] not in b_out
|
| 140 |
+
|
| 141 |
+
# Check if any suggestion targets the rare word
|
| 142 |
+
word_targeted = False
|
| 143 |
+
targeting_suggestion = None
|
| 144 |
+
for s in b_suggestions:
|
| 145 |
+
if s.get("original", "") == item["word"]:
|
| 146 |
+
word_targeted = True
|
| 147 |
+
targeting_suggestion = s
|
| 148 |
+
break
|
| 149 |
+
|
| 150 |
+
is_fp = b_changed_word or word_targeted
|
| 151 |
+
if is_fp:
|
| 152 |
+
false_positives += 1
|
| 153 |
+
|
| 154 |
+
result = {
|
| 155 |
+
"id": item["id"],
|
| 156 |
+
"word": item["word"],
|
| 157 |
+
"raw_changed": a_changed_word,
|
| 158 |
+
"pipeline_changed": b_changed_word,
|
| 159 |
+
"pipeline_targeted": word_targeted,
|
| 160 |
+
"is_false_positive": is_fp,
|
| 161 |
+
}
|
| 162 |
+
results.append(result)
|
| 163 |
+
|
| 164 |
+
status = "❌ FALSE POSITIVE" if is_fp else "✅ PRESERVED"
|
| 165 |
+
print(f"\n {item['id']}: {item['desc']}")
|
| 166 |
+
print(f" Input: '{item['sentence'][:60]}...'")
|
| 167 |
+
print(f" Raw spell: changed={a_changed_word}")
|
| 168 |
+
if a_changed_word:
|
| 169 |
+
print(f" Raw output: '{a_out[:60]}...'")
|
| 170 |
+
print(f" Pipeline: changed={b_changed_word}, targeted={word_targeted}")
|
| 171 |
+
if b_changed_word:
|
| 172 |
+
print(f" Pipeline: '{b_out[:60]}...'")
|
| 173 |
+
if targeting_suggestion:
|
| 174 |
+
print(f" Suggestion: '{targeting_suggestion.get('original','')}' → '{targeting_suggestion.get('correction','')}' (conf={targeting_suggestion.get('confidence', '?')})")
|
| 175 |
+
print(f" {status}")
|
| 176 |
+
|
| 177 |
+
raw_fp_count = sum(1 for r in results if r["raw_changed"])
|
| 178 |
+
pipeline_fp_count = false_positives
|
| 179 |
+
print(f"\n{'=' * 50}")
|
| 180 |
+
print(f" Raw model FP rate: {raw_fp_count}/{total} = {raw_fp_count/total*100:.0f}%")
|
| 181 |
+
print(f" Pipeline FP rate: {pipeline_fp_count}/{total} = {pipeline_fp_count/total*100:.0f}%")
|
| 182 |
+
|
| 183 |
+
return {
|
| 184 |
+
"total": total,
|
| 185 |
+
"raw_fp_count": raw_fp_count,
|
| 186 |
+
"raw_fp_rate": f"{raw_fp_count/total*100:.0f}%",
|
| 187 |
+
"pipeline_fp_count": pipeline_fp_count,
|
| 188 |
+
"pipeline_fp_rate": f"{pipeline_fp_count/total*100:.0f}%",
|
| 189 |
+
"results": results,
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
# ══════════════════════════════════════════════════════════════════════
|
| 194 |
+
# Phase 3.2 — Specific Word-split Cases
|
| 195 |
+
# ══════════════════════════════════════════════════════════════════════
|
| 196 |
+
def test_word_splits():
|
| 197 |
+
print("\n" + "=" * 70)
|
| 198 |
+
print("PHASE 3.2 — Specific Word-split Verification")
|
| 199 |
+
print("=" * 70)
|
| 200 |
+
|
| 201 |
+
cases = [
|
| 202 |
+
{
|
| 203 |
+
"input": "ولذالك قررت السفر",
|
| 204 |
+
"target_word": "ولذالك",
|
| 205 |
+
"expected_correct": "ولذلك",
|
| 206 |
+
"bad_split": "ولذا ذلك",
|
| 207 |
+
"desc": "ولذالك should become ولذلك, NOT 'ولذا ذلك'"
|
| 208 |
+
},
|
| 209 |
+
{
|
| 210 |
+
"input": "المستشفياتهم كبيرة",
|
| 211 |
+
"target_word": "المستشفياتهم",
|
| 212 |
+
"expected_correct": "مستشفياتهم",
|
| 213 |
+
"bad_split": "في مستشفيات هم",
|
| 214 |
+
"desc": "مستشفياتهم should NOT be split into 'في مستشفيات هم'"
|
| 215 |
+
},
|
| 216 |
+
]
|
| 217 |
+
|
| 218 |
+
results = []
|
| 219 |
+
for case in cases:
|
| 220 |
+
r = api_call("/api/analyze", case["input"])
|
| 221 |
+
corrected = r.get("corrected", case["input"])
|
| 222 |
+
suggestions = r.get("suggestions", [])
|
| 223 |
+
|
| 224 |
+
has_bad_split = case["bad_split"] in corrected
|
| 225 |
+
has_good_correction = case["expected_correct"] in corrected
|
| 226 |
+
|
| 227 |
+
result = {
|
| 228 |
+
"input": case["input"],
|
| 229 |
+
"corrected": corrected,
|
| 230 |
+
"bad_split_present": has_bad_split,
|
| 231 |
+
"good_correction_present": has_good_correction,
|
| 232 |
+
}
|
| 233 |
+
results.append(result)
|
| 234 |
+
|
| 235 |
+
print(f"\n {case['desc']}")
|
| 236 |
+
print(f" Input: '{case['input']}'")
|
| 237 |
+
print(f" Corrected: '{corrected}'")
|
| 238 |
+
if has_bad_split:
|
| 239 |
+
print(f" ❌ BAD SPLIT detected: '{case['bad_split']}'")
|
| 240 |
+
elif has_good_correction:
|
| 241 |
+
print(f" ✅ Correctly fixed to '{case['expected_correct']}'")
|
| 242 |
+
else:
|
| 243 |
+
print(f" ⚠ Neither expected correction nor bad split found")
|
| 244 |
+
|
| 245 |
+
return results
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
# ══════════════════════════════════════════════════════════════════════
|
| 249 |
+
# Phase 5.5 — Constructed Dual-correction Cases
|
| 250 |
+
# ══════════════════════════════════════════════════════════════════════
|
| 251 |
+
def test_dual_corrections():
|
| 252 |
+
print("\n" + "=" * 70)
|
| 253 |
+
print("PHASE 5.5 — Constructed Dual-correction Cases")
|
| 254 |
+
print("=" * 70)
|
| 255 |
+
|
| 256 |
+
# Cases where spelling AND grammar would both want to change words
|
| 257 |
+
cases = [
|
| 258 |
+
{
|
| 259 |
+
"input": "الطالبه كتبو الوجبات",
|
| 260 |
+
"desc": "Spelling: الطالبه→الطالبة, Grammar: كتبو→كتبوا + possibly الوجبات→الواجبات",
|
| 261 |
+
},
|
| 262 |
+
{
|
| 263 |
+
"input": "هو ذهبو الي البيت",
|
| 264 |
+
"desc": "Spelling: الي→إلى, Grammar: ذهبو→ذهب (singular subject هو)",
|
| 265 |
+
},
|
| 266 |
+
{
|
| 267 |
+
"input": "الطلاب اجتهدو في امتحانتهم",
|
| 268 |
+
"desc": "Spelling: امتحانتهم→امتحاناتهم, Grammar: اجتهدو→اجتهدوا",
|
| 269 |
+
},
|
| 270 |
+
]
|
| 271 |
+
|
| 272 |
+
results = []
|
| 273 |
+
for case in cases:
|
| 274 |
+
r = api_call("/api/analyze", case["input"])
|
| 275 |
+
corrected = r.get("corrected", case["input"])
|
| 276 |
+
suggestions = r.get("suggestions", [])
|
| 277 |
+
|
| 278 |
+
# Check for text duplication
|
| 279 |
+
words = corrected.split()
|
| 280 |
+
has_duplicate = any(i > 0 and words[i] == words[i-1] for i in range(len(words)))
|
| 281 |
+
|
| 282 |
+
# Check for dropped words (output should have ≈ same word count ±1)
|
| 283 |
+
input_words = case["input"].split()
|
| 284 |
+
word_diff = len(words) - len(input_words)
|
| 285 |
+
|
| 286 |
+
result = {
|
| 287 |
+
"input": case["input"],
|
| 288 |
+
"corrected": corrected,
|
| 289 |
+
"suggestions": len(suggestions),
|
| 290 |
+
"has_duplicate": has_duplicate,
|
| 291 |
+
"word_count_diff": word_diff,
|
| 292 |
+
}
|
| 293 |
+
results.append(result)
|
| 294 |
+
|
| 295 |
+
print(f"\n {case['desc']}")
|
| 296 |
+
print(f" Input: '{case['input']}'")
|
| 297 |
+
print(f" Corrected: '{corrected}'")
|
| 298 |
+
print(f" Suggestions: {len(suggestions)}")
|
| 299 |
+
if has_duplicate:
|
| 300 |
+
print(f" ❌ DUPLICATE WORDS detected in output!")
|
| 301 |
+
else:
|
| 302 |
+
print(f" ✅ No duplicate words")
|
| 303 |
+
if abs(word_diff) > 2:
|
| 304 |
+
print(f" ⚠ Word count diff: {word_diff} (possible drop/duplication)")
|
| 305 |
+
else:
|
| 306 |
+
print(f" ✅ Word count reasonable (diff={word_diff})")
|
| 307 |
+
|
| 308 |
+
for s in suggestions:
|
| 309 |
+
print(f" [{s.get('start')}:{s.get('end')}] {s.get('type')}: '{s.get('original','')}' → '{s.get('correction','')}'")
|
| 310 |
+
|
| 311 |
+
return results
|
| 312 |
+
|
| 313 |
+
|
| 314 |
+
# ══════════════════════════════════════════════════════════════════════
|
| 315 |
+
# Phase 6.3 — BUG-017 Re-test (Intermittent Empty Response)
|
| 316 |
+
# ══════════════════════════════════════════════════════════════════════
|
| 317 |
+
def test_bug017():
|
| 318 |
+
print("\n" + "=" * 70)
|
| 319 |
+
print("PHASE 6.3 — BUG-017 Re-test (Intermittent Empty Response)")
|
| 320 |
+
print("=" * 70)
|
| 321 |
+
|
| 322 |
+
# Send the same input 5 times rapidly and check for empty responses
|
| 323 |
+
test_input = "الحديقه جميله والأزهار متفتحه"
|
| 324 |
+
empty_count = 0
|
| 325 |
+
error_count = 0
|
| 326 |
+
results = []
|
| 327 |
+
|
| 328 |
+
for i in range(5):
|
| 329 |
+
r = api_call("/api/analyze", test_input, timeout=30)
|
| 330 |
+
corrected = r.get("corrected", "")
|
| 331 |
+
suggestions = r.get("suggestions", [])
|
| 332 |
+
status = r.get("status", "")
|
| 333 |
+
warnings = r.get("warnings", {})
|
| 334 |
+
|
| 335 |
+
is_empty = (corrected == test_input and len(suggestions) == 0)
|
| 336 |
+
is_error = "error" in r and "status" not in r
|
| 337 |
+
|
| 338 |
+
if is_empty:
|
| 339 |
+
empty_count += 1
|
| 340 |
+
if is_error:
|
| 341 |
+
error_count += 1
|
| 342 |
+
|
| 343 |
+
result = {
|
| 344 |
+
"attempt": i + 1,
|
| 345 |
+
"corrected": corrected,
|
| 346 |
+
"suggestions": len(suggestions),
|
| 347 |
+
"status": status,
|
| 348 |
+
"warnings": warnings,
|
| 349 |
+
"is_empty": is_empty,
|
| 350 |
+
"is_error": is_error,
|
| 351 |
+
}
|
| 352 |
+
results.append(result)
|
| 353 |
+
|
| 354 |
+
status_str = "❌ EMPTY" if is_empty else ("❌ ERROR" if is_error else "✅ OK")
|
| 355 |
+
print(f" Attempt {i+1}: {status_str} — suggestions={len(suggestions)}, status='{status}'")
|
| 356 |
+
if warnings:
|
| 357 |
+
print(f" Warnings: {warnings}")
|
| 358 |
+
if is_error:
|
| 359 |
+
print(f" Error: {r.get('error', '?')}")
|
| 360 |
+
|
| 361 |
+
print(f"\n Empty responses: {empty_count}/5")
|
| 362 |
+
print(f" Error responses: {error_count}/5")
|
| 363 |
+
if empty_count > 0:
|
| 364 |
+
print(f" ⚠ BUG-017 may still be present!")
|
| 365 |
+
else:
|
| 366 |
+
print(f" ✅ No empty responses detected")
|
| 367 |
+
|
| 368 |
+
return {
|
| 369 |
+
"empty_count": empty_count,
|
| 370 |
+
"error_count": error_count,
|
| 371 |
+
"results": results,
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
|
| 375 |
+
# ══════════════════════════════════════════════════════════════════════
|
| 376 |
+
# Phase 6.4 — 187-word Long Input Regression
|
| 377 |
+
# ══════════════════════════════════════════════════════════════════════
|
| 378 |
+
def test_long_input_regression():
|
| 379 |
+
print("\n" + "=" * 70)
|
| 380 |
+
print("PHASE 6.4 — 187-word Long Input Regression")
|
| 381 |
+
print("=" * 70)
|
| 382 |
+
|
| 383 |
+
long_text = (
|
| 384 |
+
"في ظل التطورات التكنولوجية المتسارعة التي يشهدها العالم اليوم أصبح من الضروري "
|
| 385 |
+
"أن نواكب هذه التغييرات ونتكيف معها بشكل فعال حيث تلعب التكنولوجيا دورا محوريا "
|
| 386 |
+
"في مختلف جوانب حياتنا اليومية بدءا من التعليم والصحة وصولا إلى الاقتصاد والسياسة "
|
| 387 |
+
"ولقد أدى الذكاء الاصطناعي إلى تحولات جذرية في طريقة عمل المؤسسات والشركات حيث "
|
| 388 |
+
"باتت الآلات قادرة على أداء مهام كانت حكرا على البشر مما يطرح تساؤلات عديدة حول "
|
| 389 |
+
"مستقبل سوق العمل والوظائف التقليدية كما أن التحول الرقمي فرض على الحكومات والمجتمعات "
|
| 390 |
+
"إعادة النظر في سياساتها التعليمية والاقتصادية لضمان مواكبة هذا التطور السريع وفي هذا "
|
| 391 |
+
"السياق يبرز دور البحث العلمي والابتكار كعاملين أساسيين في دفع عجلة التنمية المستدامة "
|
| 392 |
+
"وتحقيق الرفاهية للمجتمعات البشرية إذ لا يمكن لأي دولة أن تحقق تقدما حقيقيا دون "
|
| 393 |
+
"الاستثمار في العقول البشرية وتوفير بيئة محفزة للإبداع والابتكار ومن هنا تأتي أهمية "
|
| 394 |
+
"التعاون الدولي في مجال البحث العلمي وتبادل الخبرات والمعارف بين الدول المتقدمة والنامية "
|
| 395 |
+
"على حد سواء لتحقيق التنمية الشاملة والمستدامة التي تعود بالنفع على جميع شعوب العالم"
|
| 396 |
+
)
|
| 397 |
+
print(f" Input: {len(long_text)} chars, {len(long_text.split())} words")
|
| 398 |
+
|
| 399 |
+
r = api_call("/api/analyze", long_text, timeout=120)
|
| 400 |
+
status = r.get("status", "")
|
| 401 |
+
corrected = r.get("corrected", "")
|
| 402 |
+
suggestions = r.get("suggestions", [])
|
| 403 |
+
warnings = r.get("warnings", {})
|
| 404 |
+
timing = r.get("timing_ms", {})
|
| 405 |
+
|
| 406 |
+
if "error" in r and "status" not in r:
|
| 407 |
+
print(f" ❌ ERROR: {r['error']}")
|
| 408 |
+
result_status = "error"
|
| 409 |
+
elif status == "partial":
|
| 410 |
+
print(f" ⚠ PARTIAL: some stages failed")
|
| 411 |
+
print(f" Warnings: {warnings}")
|
| 412 |
+
result_status = "partial"
|
| 413 |
+
elif status == "success":
|
| 414 |
+
print(f" ✅ SUCCESS")
|
| 415 |
+
result_status = "success"
|
| 416 |
+
else:
|
| 417 |
+
print(f" ⚠ UNKNOWN STATUS: '{status}'")
|
| 418 |
+
result_status = "unknown"
|
| 419 |
+
|
| 420 |
+
print(f" Elapsed: {r.get('_elapsed_ms', '?')}ms")
|
| 421 |
+
print(f" Timing: {timing}")
|
| 422 |
+
print(f" Suggestions: {len(suggestions)}")
|
| 423 |
+
print(f" Corrected == Original: {corrected == long_text}")
|
| 424 |
+
|
| 425 |
+
# Key check: response should NOT be silently empty
|
| 426 |
+
is_silently_empty = (status == "success" and corrected == long_text and len(suggestions) == 0)
|
| 427 |
+
if is_silently_empty:
|
| 428 |
+
print(f" ⚠ Silently empty! This is the BUG-032 behavior we're preventing.")
|
| 429 |
+
else:
|
| 430 |
+
print(f" ✅ Response is either successful with results or properly flagged as partial/error")
|
| 431 |
+
|
| 432 |
+
return {
|
| 433 |
+
"input_chars": len(long_text),
|
| 434 |
+
"input_words": len(long_text.split()),
|
| 435 |
+
"status": result_status,
|
| 436 |
+
"suggestions": len(suggestions),
|
| 437 |
+
"warnings": warnings,
|
| 438 |
+
"timing": timing,
|
| 439 |
+
"elapsed_ms": r.get("_elapsed_ms"),
|
| 440 |
+
"is_silently_empty": is_silently_empty,
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
|
| 444 |
+
# ══════════════════════════════════════════════════════════════════════
|
| 445 |
+
# Phase 7.1 — BUG-018 Precise Tracing
|
| 446 |
+
# ══════════════════════════════════════════════════════════════════════
|
| 447 |
+
def test_bug018_tracing():
|
| 448 |
+
print("\n" + "=" * 70)
|
| 449 |
+
print("PHASE 7.1 — BUG-018 Precise Tracing (dropped ؛)")
|
| 450 |
+
print("=" * 70)
|
| 451 |
+
|
| 452 |
+
test_input = "قال المعلم للطلاب ادرسوا جيدا فالامتحان قريب"
|
| 453 |
+
print(f" Input: '{test_input}'")
|
| 454 |
+
|
| 455 |
+
# Track A: Raw punctuation only
|
| 456 |
+
a = api_call("/api/punctuation", test_input)
|
| 457 |
+
a_out = a.get("corrected_text", test_input)
|
| 458 |
+
has_semicolon_raw = "؛" in a_out
|
| 459 |
+
print(f"\n Raw punctuation output: '{a_out}'")
|
| 460 |
+
print(f" Has ؛: {has_semicolon_raw}")
|
| 461 |
+
|
| 462 |
+
# Track B: Full pipeline
|
| 463 |
+
b = api_call("/api/analyze", test_input)
|
| 464 |
+
b_out = b.get("corrected", test_input)
|
| 465 |
+
b_sugg = b.get("suggestions", [])
|
| 466 |
+
has_semicolon_pipeline = "؛" in b_out
|
| 467 |
+
print(f"\n Pipeline output: '{b_out}'")
|
| 468 |
+
print(f" Has ؛: {has_semicolon_pipeline}")
|
| 469 |
+
print(f" Suggestions: {len(b_sugg)}")
|
| 470 |
+
|
| 471 |
+
for s in b_sugg:
|
| 472 |
+
print(f" [{s.get('start')}:{s.get('end')}] {s.get('type')}: '{s.get('original','')}' → '{s.get('correction','')}'")
|
| 473 |
+
|
| 474 |
+
# Determine drop cause
|
| 475 |
+
if has_semicolon_raw and not has_semicolon_pipeline:
|
| 476 |
+
# Raw produced it but pipeline dropped it
|
| 477 |
+
punc_suggestions = [s for s in b_sugg if s.get('type') == 'punctuation']
|
| 478 |
+
total_punc = len(punc_suggestions)
|
| 479 |
+
if total_punc >= 3:
|
| 480 |
+
cause = "MAX_PUNC_PATCHES_PER_RESPONSE cap (3 patches, ؛ was 4th+)"
|
| 481 |
+
else:
|
| 482 |
+
# Check if any grammar suggestion overlaps the ؛ position
|
| 483 |
+
cause = "StageLocker or validate_punctuation_diff rejection"
|
| 484 |
+
print(f"\n DIAGNOSIS: ؛ was produced by raw model but dropped by pipeline")
|
| 485 |
+
print(f" Likely cause: {cause}")
|
| 486 |
+
elif not has_semicolon_raw:
|
| 487 |
+
cause = "Raw punctuation model did NOT produce ؛ at all"
|
| 488 |
+
print(f"\n DIAGNOSIS: {cause} — not a pipeline bug")
|
| 489 |
+
else:
|
| 490 |
+
cause = "؛ present in both raw and pipeline — BUG-018 not reproduced"
|
| 491 |
+
print(f"\n DIAGNOSIS: {cause}")
|
| 492 |
+
|
| 493 |
+
return {
|
| 494 |
+
"input": test_input,
|
| 495 |
+
"raw_output": a_out,
|
| 496 |
+
"pipeline_output": b_out,
|
| 497 |
+
"has_semicolon_raw": has_semicolon_raw,
|
| 498 |
+
"has_semicolon_pipeline": has_semicolon_pipeline,
|
| 499 |
+
"diagnosis": cause,
|
| 500 |
+
"pipeline_punc_count": len([s for s in b_sugg if s.get('type') == 'punctuation']),
|
| 501 |
+
}
|
| 502 |
+
|
| 503 |
+
|
| 504 |
+
# ══════════════════════════════════════════════════════════════════════
|
| 505 |
+
# MAIN
|
| 506 |
+
# ══════════════════════════════════════════════════════════════════════
|
| 507 |
+
if __name__ == "__main__":
|
| 508 |
+
print("BAYAN — Gap-filling Live Tests\n")
|
| 509 |
+
|
| 510 |
+
all_results["phase_1_3"] = test_category9_live()
|
| 511 |
+
all_results["phase_2"] = test_rare_vocabulary()
|
| 512 |
+
all_results["phase_3_2"] = test_word_splits()
|
| 513 |
+
all_results["phase_5_5"] = test_dual_corrections()
|
| 514 |
+
all_results["phase_6_3"] = test_bug017()
|
| 515 |
+
all_results["phase_6_4"] = test_long_input_regression()
|
| 516 |
+
all_results["phase_7_1"] = test_bug018_tracing()
|
| 517 |
+
|
| 518 |
+
# Save all results
|
| 519 |
+
output_path = os.path.join(os.path.dirname(__file__), 'gap_filling_results.json')
|
| 520 |
+
with open(output_path, 'w', encoding='utf-8') as f:
|
| 521 |
+
json.dump(all_results, f, ensure_ascii=False, indent=2)
|
| 522 |
+
print(f"\n\nAll results saved to {output_path}")
|
archive/old_tests/phase0_investigation.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Phase 0 — Investigation Script
|
| 3 |
+
Tests:
|
| 4 |
+
0.1 — ان→أن in sentence context vs isolation
|
| 5 |
+
0.3 — BUG-032 (long text) with detailed error capture
|
| 6 |
+
0.4 — BUG-031 sentence (already resolved: الطلاب = plural → اللذين is wrong)
|
| 7 |
+
"""
|
| 8 |
+
import sys, os, json, time, requests
|
| 9 |
+
|
| 10 |
+
API_BASE = "https://bayan10-bayan-api.hf.space"
|
| 11 |
+
TIMEOUT = 90
|
| 12 |
+
|
| 13 |
+
def api_call(endpoint, text):
|
| 14 |
+
url = f"{API_BASE}{endpoint}"
|
| 15 |
+
try:
|
| 16 |
+
t0 = time.time()
|
| 17 |
+
resp = requests.post(url, json={"text": text}, timeout=TIMEOUT)
|
| 18 |
+
elapsed = int((time.time() - t0) * 1000)
|
| 19 |
+
if resp.status_code == 200:
|
| 20 |
+
data = resp.json()
|
| 21 |
+
data['_elapsed_ms'] = elapsed
|
| 22 |
+
return data
|
| 23 |
+
return {"error": f"HTTP {resp.status_code}", "_elapsed_ms": elapsed}
|
| 24 |
+
except Exception as e:
|
| 25 |
+
return {"error": f"{type(e).__name__}: {e}"}
|
| 26 |
+
|
| 27 |
+
def test_0_1():
|
| 28 |
+
"""0.1 — Does spelling correct ان→أن in sentence context?"""
|
| 29 |
+
print("=" * 70)
|
| 30 |
+
print("PHASE 0.1 — ان→أن contradiction test")
|
| 31 |
+
print("=" * 70)
|
| 32 |
+
|
| 33 |
+
tests = [
|
| 34 |
+
("ان (isolation)", "ان"),
|
| 35 |
+
("ان الحياة جميلة (sentence)", "ان الحياة جميلة"),
|
| 36 |
+
("ان الذكاء مهم (sentence)", "ان الذكاء مهم"),
|
| 37 |
+
("قال ان الحق واضح (mid-sentence)", "قال ان الحق واضح"),
|
| 38 |
+
]
|
| 39 |
+
|
| 40 |
+
results = []
|
| 41 |
+
for label, text in tests:
|
| 42 |
+
# Track A: raw spelling model
|
| 43 |
+
a = api_call("/api/spelling", text)
|
| 44 |
+
a_out = a.get("corrected_text", text)
|
| 45 |
+
a_changed = a_out != text
|
| 46 |
+
|
| 47 |
+
# Track B: full pipeline
|
| 48 |
+
b = api_call("/api/analyze", text)
|
| 49 |
+
b_out = b.get("corrected", text)
|
| 50 |
+
b_sugg = b.get("suggestions", [])
|
| 51 |
+
|
| 52 |
+
result = {
|
| 53 |
+
"label": label, "input": text,
|
| 54 |
+
"raw_spelling": a_out, "raw_changed": a_changed,
|
| 55 |
+
"pipeline_corrected": b_out,
|
| 56 |
+
"pipeline_suggestions": len(b_sugg),
|
| 57 |
+
}
|
| 58 |
+
results.append(result)
|
| 59 |
+
|
| 60 |
+
print(f"\n {label}:")
|
| 61 |
+
print(f" Input: '{text}'")
|
| 62 |
+
print(f" Raw spell: '{a_out}' (changed={a_changed})")
|
| 63 |
+
print(f" Pipeline: '{b_out}' (suggestions={len(b_sugg)})")
|
| 64 |
+
|
| 65 |
+
# Check if ان was corrected to أن or إن
|
| 66 |
+
if 'أن' in a_out or 'إن' in a_out:
|
| 67 |
+
print(f" ✅ Raw spelling DID correct ان")
|
| 68 |
+
elif a_changed:
|
| 69 |
+
print(f" ⚠ Raw spelling changed but NOT to أن/إن")
|
| 70 |
+
else:
|
| 71 |
+
print(f" ❌ Raw spelling did NOT correct ان")
|
| 72 |
+
|
| 73 |
+
# Verdict
|
| 74 |
+
print("\n" + "-" * 50)
|
| 75 |
+
isolation = results[0]
|
| 76 |
+
sentences = results[1:]
|
| 77 |
+
iso_fixed = 'أن' in isolation['raw_spelling'] or 'إن' in isolation['raw_spelling']
|
| 78 |
+
sent_fixed = any('أن' in r['raw_spelling'] or 'إن' in r['raw_spelling'] for r in sentences)
|
| 79 |
+
|
| 80 |
+
if iso_fixed and sent_fixed:
|
| 81 |
+
verdict = "WORKS in both isolation AND sentence context"
|
| 82 |
+
elif iso_fixed and not sent_fixed:
|
| 83 |
+
verdict = "WORKS in isolation ONLY, FAILS in sentence context"
|
| 84 |
+
elif not iso_fixed:
|
| 85 |
+
verdict = "FAILS in both isolation and sentence context"
|
| 86 |
+
else:
|
| 87 |
+
verdict = "Inconsistent"
|
| 88 |
+
|
| 89 |
+
print(f" FINAL VERDICT: {verdict}")
|
| 90 |
+
return {"verdict": verdict, "results": results}
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def test_0_3():
|
| 94 |
+
"""0.3 — BUG-032: Long text (187 words / 1104 chars)"""
|
| 95 |
+
print("\n" + "=" * 70)
|
| 96 |
+
print("PHASE 0.3 — BUG-032 long text test")
|
| 97 |
+
print("=" * 70)
|
| 98 |
+
|
| 99 |
+
# 187-word Arabic text (from deep-dive report)
|
| 100 |
+
long_text = (
|
| 101 |
+
"في ظل التطورات التكنولوجية المتسارعة التي يشهدها العالم اليوم أصبح من الضروري "
|
| 102 |
+
"أن نواكب هذه التغييرات ونتكيف معها بشكل فعال حيث تلعب التكنولوجيا دورا محوريا "
|
| 103 |
+
"في مختلف جوانب حياتنا اليومية بدءا من التعليم والصحة وصولا إلى الاقتصاد والسياسة "
|
| 104 |
+
"ولقد أدى الذكاء الاصطناعي إلى تحولات جذرية في طريقة عمل المؤسسات والشركات حيث "
|
| 105 |
+
"باتت الآلات قادرة على أداء مهام كانت حكرا على البشر مما يطرح تساؤلات عديدة حول "
|
| 106 |
+
"مستقبل سوق العمل والوظائف التقليدية كما أن التحول الرقمي فرض على الحكومات والمجتمعات "
|
| 107 |
+
"إعادة النظر في سياساتها التعليمية والاقتصادية لضمان مواكبة هذا التطور السريع وفي هذا "
|
| 108 |
+
"السياق يبرز دور البحث العلمي والابتكار كعاملين أساسيين في دفع عجلة التنمية المستدامة "
|
| 109 |
+
"وتحقيق الرفاهية للمجتمعات البشرية إذ لا يمكن لأي دولة أن تحقق تقدما حقيقيا دون "
|
| 110 |
+
"الاستثمار في العقول ��لبشرية وتوفير بيئة محفزة للإبداع والابتكار ومن هنا تأتي أهمية "
|
| 111 |
+
"التعاون الدولي في مجال البحث العلمي وتبادل الخبرات والمعارف بين الدول المتقدمة والنامية "
|
| 112 |
+
"على حد سواء لتحقيق التنمية الشاملة والمستدامة التي تعود بالنفع على جميع شعوب العالم"
|
| 113 |
+
)
|
| 114 |
+
print(f" Input length: {len(long_text)} chars, {len(long_text.split())} words")
|
| 115 |
+
|
| 116 |
+
# Test all three individual endpoints
|
| 117 |
+
print("\n Testing /api/spelling...")
|
| 118 |
+
a_spell = api_call("/api/spelling", long_text)
|
| 119 |
+
print(f" Status: {'error' if 'error' in a_spell else 'OK'}")
|
| 120 |
+
if 'error' in a_spell:
|
| 121 |
+
print(f" Error: {a_spell['error']}")
|
| 122 |
+
else:
|
| 123 |
+
print(f" Elapsed: {a_spell.get('_elapsed_ms', '?')}ms")
|
| 124 |
+
print(f" Changed: {a_spell.get('corrected_text', '') != long_text}")
|
| 125 |
+
|
| 126 |
+
print("\n Testing /api/grammar...")
|
| 127 |
+
a_gram = api_call("/api/grammar", long_text)
|
| 128 |
+
print(f" Status: {'error' if 'error' in a_gram else 'OK'}")
|
| 129 |
+
if 'error' in a_gram:
|
| 130 |
+
print(f" Error: {a_gram['error']}")
|
| 131 |
+
else:
|
| 132 |
+
print(f" Elapsed: {a_gram.get('_elapsed_ms', '?')}ms")
|
| 133 |
+
print(f" Changed: {a_gram.get('corrected_text', '') != long_text}")
|
| 134 |
+
|
| 135 |
+
print("\n Testing /api/punctuation...")
|
| 136 |
+
a_punc = api_call("/api/punctuation", long_text)
|
| 137 |
+
print(f" Status: {'error' if 'error' in a_punc else 'OK'}")
|
| 138 |
+
if 'error' in a_punc:
|
| 139 |
+
print(f" Error: {a_punc['error']}")
|
| 140 |
+
else:
|
| 141 |
+
print(f" Elapsed: {a_punc.get('_elapsed_ms', '?')}ms")
|
| 142 |
+
print(f" Changed: {a_punc.get('corrected_text', '') != long_text}")
|
| 143 |
+
|
| 144 |
+
print("\n Testing /api/analyze (full pipeline)...")
|
| 145 |
+
b = api_call("/api/analyze", long_text)
|
| 146 |
+
print(f" Status: {'error' if 'error' in b and 'status' not in b else b.get('status', '?')}")
|
| 147 |
+
if 'error' in b and 'status' not in b:
|
| 148 |
+
print(f" Error: {b['error']}")
|
| 149 |
+
else:
|
| 150 |
+
print(f" Elapsed: {b.get('_elapsed_ms', '?')}ms")
|
| 151 |
+
print(f" Suggestions: {len(b.get('suggestions', []))}")
|
| 152 |
+
print(f" Timing: {b.get('timing_ms', {})}")
|
| 153 |
+
if b.get('corrected') == long_text:
|
| 154 |
+
print(f" ⚠ corrected == original (no changes or silent failure?)")
|
| 155 |
+
|
| 156 |
+
return {
|
| 157 |
+
"input_chars": len(long_text),
|
| 158 |
+
"input_words": len(long_text.split()),
|
| 159 |
+
"spelling": {"error": a_spell.get("error"), "elapsed": a_spell.get("_elapsed_ms")},
|
| 160 |
+
"grammar": {"error": a_gram.get("error"), "elapsed": a_gram.get("_elapsed_ms")},
|
| 161 |
+
"punctuation": {"error": a_punc.get("error"), "elapsed": a_punc.get("_elapsed_ms")},
|
| 162 |
+
"pipeline": {
|
| 163 |
+
"error": b.get("error"),
|
| 164 |
+
"status": b.get("status"),
|
| 165 |
+
"suggestions": len(b.get("suggestions", [])),
|
| 166 |
+
"timing": b.get("timing_ms", {}),
|
| 167 |
+
"elapsed": b.get("_elapsed_ms"),
|
| 168 |
+
}
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def test_0_4():
|
| 173 |
+
"""0.4 — BUG-031: اللذين vs الذين"""
|
| 174 |
+
print("\n" + "=" * 70)
|
| 175 |
+
print("PHASE 0.4 — BUG-031 (اللذين vs الذين)")
|
| 176 |
+
print("=" * 70)
|
| 177 |
+
|
| 178 |
+
sentence = "الطلاب اللذين اجتهدو في دراستهم حققو نتائج ممتازه في الأمتحانات الصعبه"
|
| 179 |
+
print(f" Test sentence: '{sentence}'")
|
| 180 |
+
print(f" Subject: الطلاب (PLURAL, not dual)")
|
| 181 |
+
print(f" Therefore: اللذين (dual) is WRONG, الذين (plural) is CORRECT")
|
| 182 |
+
print(f" Verdict: BUG-031 IS a real bug — grammar should correct اللذين→الذين")
|
| 183 |
+
|
| 184 |
+
# Test it
|
| 185 |
+
a_gram = api_call("/api/grammar", sentence)
|
| 186 |
+
a_out = a_gram.get("corrected_text", sentence)
|
| 187 |
+
print(f"\n Grammar model output: '{a_out}'")
|
| 188 |
+
if 'الذين' in a_out and 'اللذين' not in a_out:
|
| 189 |
+
print(f" ✅ Grammar DID correct اللذين→الذين")
|
| 190 |
+
bug_status = "fixed_by_model"
|
| 191 |
+
elif 'اللذين' in a_out:
|
| 192 |
+
print(f" ❌ Grammar did NOT correct اللذين (left as dual)")
|
| 193 |
+
bug_status = "still_broken"
|
| 194 |
+
else:
|
| 195 |
+
print(f" ⚠ Unexpected output")
|
| 196 |
+
bug_status = "unclear"
|
| 197 |
+
|
| 198 |
+
return {
|
| 199 |
+
"sentence": sentence,
|
| 200 |
+
"subject": "الطلاب (PLURAL)",
|
| 201 |
+
"correct_form": "الذين (plural)",
|
| 202 |
+
"is_real_bug": True,
|
| 203 |
+
"grammar_output": a_out,
|
| 204 |
+
"bug_status": bug_status,
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
if __name__ == "__main__":
|
| 209 |
+
print("BAYAN Phase 0 — Investigation\n")
|
| 210 |
+
|
| 211 |
+
all_results = {}
|
| 212 |
+
|
| 213 |
+
all_results["phase_0_1"] = test_0_1()
|
| 214 |
+
all_results["phase_0_3"] = test_0_3()
|
| 215 |
+
all_results["phase_0_4"] = test_0_4()
|
| 216 |
+
|
| 217 |
+
# Save results
|
| 218 |
+
output_path = os.path.join(os.path.dirname(__file__), 'phase0_results.json')
|
| 219 |
+
with open(output_path, 'w', encoding='utf-8') as f:
|
| 220 |
+
json.dump(all_results, f, ensure_ascii=False, indent=2)
|
| 221 |
+
print(f"\nResults saved to {output_path}")
|
archive/old_tests/phase0_results.json
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"phase_0_1": {
|
| 3 |
+
"verdict": "WORKS in isolation ONLY, FAILS in sentence context",
|
| 4 |
+
"results": [
|
| 5 |
+
{
|
| 6 |
+
"label": "ان (isolation)",
|
| 7 |
+
"input": "ان",
|
| 8 |
+
"raw_spelling": "أن",
|
| 9 |
+
"raw_changed": true,
|
| 10 |
+
"pipeline_corrected": "إن.",
|
| 11 |
+
"pipeline_suggestions": 1
|
| 12 |
+
},
|
| 13 |
+
{
|
| 14 |
+
"label": "ان الحياة جميلة (sentence)",
|
| 15 |
+
"input": "ان الحياة جميلة",
|
| 16 |
+
"raw_spelling": "ان الحياة جميلة",
|
| 17 |
+
"raw_changed": false,
|
| 18 |
+
"pipeline_corrected": "إن الحياة جميلة!",
|
| 19 |
+
"pipeline_suggestions": 2
|
| 20 |
+
},
|
| 21 |
+
{
|
| 22 |
+
"label": "ان الذكاء مهم (sentence)",
|
| 23 |
+
"input": "ان الذكاء مهم",
|
| 24 |
+
"raw_spelling": "ان الذكاء مهم",
|
| 25 |
+
"raw_changed": false,
|
| 26 |
+
"pipeline_corrected": "إن الذكاء مهم.",
|
| 27 |
+
"pipeline_suggestions": 2
|
| 28 |
+
},
|
| 29 |
+
{
|
| 30 |
+
"label": "قال ان الحق واضح (mid-sentence)",
|
| 31 |
+
"input": "قال ان الحق واضح",
|
| 32 |
+
"raw_spelling": "قال ان الحق واضح",
|
| 33 |
+
"raw_changed": false,
|
| 34 |
+
"pipeline_corrected": "قال ان: الحق واضح",
|
| 35 |
+
"pipeline_suggestions": 1
|
| 36 |
+
}
|
| 37 |
+
]
|
| 38 |
+
},
|
| 39 |
+
"phase_0_3": {
|
| 40 |
+
"input_chars": 982,
|
| 41 |
+
"input_words": 159,
|
| 42 |
+
"spelling": {
|
| 43 |
+
"error": "ReadTimeout: HTTPSConnectionPool(host='bayan10-bayan-api.hf.space', port=443): Read timed out. (read timeout=90)",
|
| 44 |
+
"elapsed": null
|
| 45 |
+
},
|
| 46 |
+
"grammar": {
|
| 47 |
+
"error": "ReadTimeout: HTTPSConnectionPool(host='bayan10-bayan-api.hf.space', port=443): Read timed out. (read timeout=90)",
|
| 48 |
+
"elapsed": null
|
| 49 |
+
},
|
| 50 |
+
"punctuation": {
|
| 51 |
+
"error": "ReadTimeout: HTTPSConnectionPool(host='bayan10-bayan-api.hf.space', port=443): Read timed out. (read timeout=90)",
|
| 52 |
+
"elapsed": null
|
| 53 |
+
},
|
| 54 |
+
"pipeline": {
|
| 55 |
+
"error": null,
|
| 56 |
+
"status": "success",
|
| 57 |
+
"suggestions": 4,
|
| 58 |
+
"timing": {
|
| 59 |
+
"grammar_ms": 12179,
|
| 60 |
+
"punctuation_ms": 12237,
|
| 61 |
+
"spelling_ms": 0,
|
| 62 |
+
"total_ms": 24420
|
| 63 |
+
},
|
| 64 |
+
"elapsed": 54892
|
| 65 |
+
}
|
| 66 |
+
},
|
| 67 |
+
"phase_0_4": {
|
| 68 |
+
"sentence": "الطلاب اللذين اجتهدو في دراستهم حققو نتائج ممتازه في الأمتحانات الصعبه",
|
| 69 |
+
"subject": "الطلاب (PLURAL)",
|
| 70 |
+
"correct_form": "الذين (plural)",
|
| 71 |
+
"is_real_bug": true,
|
| 72 |
+
"grammar_output": "الطلاب الذين اجتهدو في دراستهم حققوا نتائج ممتازة في الامتحانات الصعبة",
|
| 73 |
+
"bug_status": "fixed_by_model"
|
| 74 |
+
}
|
| 75 |
+
}
|
archive/old_tests/phase10_helpers/audit_output.txt
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
=== Section 1 & 2: Counts & Categories ===
|
| 2 |
+
|
| 3 |
+
Spelling (80 samples):
|
| 4 |
+
hamza: 25
|
| 5 |
+
hamza_prefix: 5
|
| 6 |
+
ta_marbuta: 10
|
| 7 |
+
ta_marbuta_prefix: 5
|
| 8 |
+
alif_maqsura: 8
|
| 9 |
+
word_split: 7
|
| 10 |
+
correct_text: 15
|
| 11 |
+
multi_error: 5
|
| 12 |
+
|
| 13 |
+
Grammar (45 samples):
|
| 14 |
+
sv_agree: 10
|
| 15 |
+
gender: 5
|
| 16 |
+
case: 5
|
| 17 |
+
five_nouns: 4
|
| 18 |
+
dual: 2
|
| 19 |
+
nasb: 4
|
| 20 |
+
correct: 15
|
| 21 |
+
|
| 22 |
+
Punctuation (20 samples):
|
| 23 |
+
missing_period: 3
|
| 24 |
+
missing_question: 3
|
| 25 |
+
missing_comma: 2
|
| 26 |
+
missing_multi: 2
|
| 27 |
+
already_correct: 5
|
| 28 |
+
word_preservation: 2
|
| 29 |
+
dialogue: 1
|
| 30 |
+
enumeration: 1
|
| 31 |
+
exclamation: 1
|
| 32 |
+
|
| 33 |
+
Entities (30 samples):
|
| 34 |
+
person: 10
|
| 35 |
+
place: 8
|
| 36 |
+
company: 5
|
| 37 |
+
tech: 7
|
| 38 |
+
|
| 39 |
+
Religious (30 samples):
|
| 40 |
+
basmalah: 1
|
| 41 |
+
fatiha: 3
|
| 42 |
+
ikhlas: 1
|
| 43 |
+
qadr: 1
|
| 44 |
+
falaq: 1
|
| 45 |
+
nas: 1
|
| 46 |
+
baqara: 2
|
| 47 |
+
kursi: 1
|
| 48 |
+
shahada: 2
|
| 49 |
+
hadith: 5
|
| 50 |
+
dua: 4
|
| 51 |
+
hamdalah: 1
|
| 52 |
+
tasbih: 1
|
| 53 |
+
salawat: 1
|
| 54 |
+
istighfar: 1
|
| 55 |
+
takbir: 1
|
| 56 |
+
inna: 1
|
| 57 |
+
bismillah: 1
|
| 58 |
+
salam: 1
|
| 59 |
+
|
| 60 |
+
Structured (35 samples):
|
| 61 |
+
url: 4
|
| 62 |
+
email: 3
|
| 63 |
+
date: 3
|
| 64 |
+
time: 3
|
| 65 |
+
number: 3
|
| 66 |
+
currency: 2
|
| 67 |
+
measurement: 3
|
| 68 |
+
code: 3
|
| 69 |
+
sql: 1
|
| 70 |
+
json: 1
|
| 71 |
+
hashtag: 2
|
| 72 |
+
mention: 2
|
| 73 |
+
phone: 2
|
| 74 |
+
ip: 1
|
| 75 |
+
version: 1
|
| 76 |
+
filepath: 1
|
| 77 |
+
|
| 78 |
+
Hallucination (30 samples):
|
| 79 |
+
news: 5
|
| 80 |
+
academic: 5
|
| 81 |
+
technical: 3
|
| 82 |
+
legal: 2
|
| 83 |
+
literary: 3
|
| 84 |
+
correct_simple: 7
|
| 85 |
+
correct_compound: 5
|
| 86 |
+
|
| 87 |
+
=== Section 3: Lengths ===
|
| 88 |
+
Spelling: Avg=3.4, Med=3, Max=5, Min=2 | 1w:0, <5:80, <15:0, <30:0, >30:0
|
| 89 |
+
Grammar: Avg=3.7, Med=4, Max=5, Min=3 | 1w:0, <5:45, <15:0, <30:0, >30:0
|
| 90 |
+
Punctuation: Avg=5.3, Med=5, Max=8, Min=4 | 1w:0, <5:12, <15:8, <30:0, >30:0
|
| 91 |
+
Entities: Avg=4.2, Med=4, Max=6, Min=3 | 1w:0, <5:29, <15:1, <30:0, >30:0
|
| 92 |
+
Religious: Avg=6.9, Med=7, Max=12, Min=4 | 1w:0, <5:11, <15:19, <30:0, >30:0
|
| 93 |
+
Structured: Avg=4.9, Med=5, Max=9, Min=2 | 1w:0, <5:24, <15:11, <30:0, >30:0
|
| 94 |
+
Hallucination: Avg=8.7, Med=10, Max=12, Min=4 | 1w:0, <5:5, <15:25, <30:0, >30:0
|
| 95 |
+
|
| 96 |
+
=== Section 4: Synthetic Patterns ===
|
| 97 |
+
Spelling: 0 exact duplicates. Unique=80/80
|
| 98 |
+
Grammar: 0 exact duplicates. Unique=45/45
|
| 99 |
+
Punctuation: 0 exact duplicates. Unique=20/20
|
| 100 |
+
Entities: 0 exact duplicates. Unique=30/30
|
| 101 |
+
Religious: 0 exact duplicates. Unique=30/30
|
| 102 |
+
Structured: 0 exact duplicates. Unique=35/35
|
| 103 |
+
Hallucination: 0 exact duplicates. Unique=30/30
|
| 104 |
+
|
| 105 |
+
=== Section 10: Random Samples for Review ===
|
| 106 |
+
|
| 107 |
+
--- Spelling (20 samples) ---
|
| 108 |
+
[1] ID: S015 | Cat: hamza
|
| 109 |
+
In : اننا نحب الوطن
|
| 110 |
+
Exp: إننا نحب الوطن
|
| 111 |
+
[2] ID: S004 | Cat: hamza
|
| 112 |
+
In : لان الأمر يتعلق بالمستقبل
|
| 113 |
+
Exp: لأن الأمر يتعلق بالمستقبل
|
| 114 |
+
[3] ID: S036 | Cat: ta_marbuta
|
| 115 |
+
In : المكتبه قريبه من البيت
|
| 116 |
+
Exp: المكتبة قريبة من البيت
|
| 117 |
+
[4] ID: S032 | Cat: ta_marbuta
|
| 118 |
+
In : الجامعه في القاهره
|
| 119 |
+
Exp: الجامعة في القاهرة
|
| 120 |
+
[5] ID: S029 | Cat: hamza_prefix
|
| 121 |
+
In : كالاطفال في اللعب
|
| 122 |
+
Exp: كالأطفال في اللعب
|
| 123 |
+
[6] ID: S018 | Cat: hamza
|
| 124 |
+
In : ارسل الرسالة فوراً
|
| 125 |
+
Exp: أرسل الرسالة فوراً
|
| 126 |
+
[7] ID: S014 | Cat: hamza
|
| 127 |
+
In : انت طالب مجتهد
|
| 128 |
+
Exp: أنت طالب مجتهد
|
| 129 |
+
[8] ID: S070 | Cat: correct_text
|
| 130 |
+
In : العلم نور والجهل ظلام
|
| 131 |
+
Exp: العلم نور والجهل ظلام
|
| 132 |
+
[9] ID: S012 | Cat: hamza
|
| 133 |
+
In : اخيراً وصلنا إلى الهدف
|
| 134 |
+
Exp: أخيراً وصلنا إلى الهدف
|
| 135 |
+
[10] ID: S055 | Cat: word_split
|
| 136 |
+
In : خرج منالمدرسة
|
| 137 |
+
Exp: خرج من المدرسة
|
| 138 |
+
[11] ID: S005 | Cat: hamza
|
| 139 |
+
In : اين ذهبت أمس
|
| 140 |
+
Exp: أين ذهبت أمس
|
| 141 |
+
[12] ID: S079 | Cat: multi_error
|
| 142 |
+
In : اين الجامعه الكبيره
|
| 143 |
+
Exp: أين الجامعة الكبيرة
|
| 144 |
+
[13] ID: S072 | Cat: correct_text
|
| 145 |
+
In : المعلم يشرح الدرس
|
| 146 |
+
Exp: ╪º┘ä┘à╪╣┘ä┘à ┘è╪┤╪▒╪¡ ���º┘ä╪»╪▒╪│
|
| 147 |
+
[14] ID: S028 | Cat: hamza_prefix
|
| 148 |
+
In : فالانسان يحتاج للعلم
|
| 149 |
+
Exp: فالإنسان يحتاج للعلم
|
| 150 |
+
[15] ID: S030 | Cat: hamza_prefix
|
| 151 |
+
In : للاسف لم ينجح
|
| 152 |
+
Exp: للأسف لم ينجح
|
| 153 |
+
[16] ID: S065 | Cat: correct_text
|
| 154 |
+
In : إلى اللقاء يا صديقي
|
| 155 |
+
Exp: إلى اللقاء يا صديقي
|
| 156 |
+
[17] ID: S069 | Cat: correct_text
|
| 157 |
+
In : الطالب المجتهد ينجح دائماً
|
| 158 |
+
Exp: الطالب المجتهد ينجح دائماً
|
| 159 |
+
[18] ID: S078 | Cat: multi_error
|
| 160 |
+
In : لان المدرسه بعيده جداً
|
| 161 |
+
Exp: لأن المدرسة بعيدة جداً
|
| 162 |
+
[19] ID: S013 | Cat: hamza
|
| 163 |
+
In : وقف امام المدرسة
|
| 164 |
+
Exp: وقف أمام المدرسة
|
| 165 |
+
[20] ID: S046 | Cat: alif_maqsura
|
| 166 |
+
In : ذهبت الي المكتبة
|
| 167 |
+
Exp: ذهبت إلى المكتبة
|
| 168 |
+
|
| 169 |
+
--- Grammar (20 samples) ---
|
| 170 |
+
[1] ID: G042 | Cat: correct
|
| 171 |
+
In : الأطفال يلعبون في الحديقة
|
| 172 |
+
Fix:
|
| 173 |
+
[2] ID: G035 | Cat: correct
|
| 174 |
+
In : ذهبت البنات إلى المدرسة
|
| 175 |
+
Fix:
|
| 176 |
+
[3] ID: G027 | Cat: nasb
|
| 177 |
+
In : لن يذهبون إلى المدرسة
|
| 178 |
+
Fix: يذهبوا
|
| 179 |
+
[4] ID: G015 | Cat: gender
|
| 180 |
+
In : الشمس مشرق اليوم
|
| 181 |
+
Fix: مشرقة
|
| 182 |
+
[5] ID: G029 | Cat: nasb
|
| 183 |
+
In : كي يتعلمون الدرس
|
| 184 |
+
Fix: يتعلموا
|
| 185 |
+
[6] ID: G038 | Cat: correct
|
| 186 |
+
In : يدرس الطالب في مكتبته
|
| 187 |
+
Fix:
|
| 188 |
+
[7] ID: G018 | Cat: case
|
| 189 |
+
In : إلى المسافرون في المطار
|
| 190 |
+
Fix: المسافرين
|
| 191 |
+
[8] ID: G001 | Cat: sv_agree
|
| 192 |
+
In : البنات ذهب إلى المدرسة
|
| 193 |
+
Fix: ذهبن/ذهبت
|
| 194 |
+
[9] ID: G011 | Cat: gender
|
| 195 |
+
In : السيارة جميل جداً
|
| 196 |
+
Fix: جميلة
|
| 197 |
+
[10] ID: G028 | Cat: nasb
|
| 198 |
+
In : لم يفعلون الواجب بعد
|
| 199 |
+
Fix: يفعلوا
|
| 200 |
+
[11] ID: G022 | Cat: five_nouns
|
| 201 |
+
In : رأيت أخوك في المسجد
|
| 202 |
+
Fix: أخاك
|
| 203 |
+
[12] ID: G039 | Cat: correct
|
| 204 |
+
In : تعمل المرأة في الشركة
|
| 205 |
+
Fix:
|
| 206 |
+
[13] ID: G010 | Cat: sv_agree
|
| 207 |
+
In : الطالبات كتب الواجب
|
| 208 |
+
Fix: كتبن
|
| 209 |
+
[14] ID: G014 | Cat: gender
|
| 210 |
+
In : المدينة كبير وواسع
|
| 211 |
+
Fix: كبيرة وواسعة
|
| 212 |
+
[15] ID: G031 | Cat: correct
|
| 213 |
+
In : ذهب الطالب إلى المدرسة
|
| 214 |
+
Fix:
|
| 215 |
+
[16] ID: G025 | Cat: dual
|
| 216 |
+
In : هذان الطالبتان مجتهدتان
|
| 217 |
+
Fix: هاتان
|
| 218 |
+
[17] ID: G037 | Cat: correct
|
| 219 |
+
In : ذهب الرجل إلى عمله
|
| 220 |
+
Fix:
|
| 221 |
+
[18] ID: G004 | Cat: sv_agree
|
| 222 |
+
In : الرجال يعمل في المصنع
|
| 223 |
+
Fix: يعملون
|
| 224 |
+
[19] ID: G003 | Cat: sv_agree
|
| 225 |
+
In : المهندسون حضر الاجتماع
|
| 226 |
+
Fix: حضروا
|
| 227 |
+
[20] ID: G013 | Cat: gender
|
| 228 |
+
In : الطالبة متفوق في دراسته
|
| 229 |
+
Fix: متفوقة/دراستها
|
| 230 |
+
|
| 231 |
+
--- Punctuation (10 samples) ---
|
| 232 |
+
[1] ID: P004 | Cat: missing_multi
|
| 233 |
+
In : كيف حالك أنا بخير والحمد لله
|
| 234 |
+
[2] ID: P012 | Cat: already_correct
|
| 235 |
+
In : كيف حالك؟ أنا بخير.
|
| 236 |
+
[3] ID: P019 | Cat: enumeration
|
| 237 |
+
In : أحتاج إلى خبز ولبن وجبن وبيض
|
| 238 |
+
[4] ID: P009 | Cat: missing_comma
|
| 239 |
+
In : جاء أحمد ومحمد وعلي
|
| 240 |
+
[5] ID: P002 | Cat: missing_question
|
| 241 |
+
In : هل أنت بخير يا صديقي
|
| 242 |
+
[6] ID: P018 | Cat: dialogue
|
| 243 |
+
In : قال أحمد أنا سعيد بلقائك يا صديقي
|
| 244 |
+
[7] ID: P008 | Cat: missing_question
|
| 245 |
+
In : لماذا لم تحضر أمس
|
| 246 |
+
[8] ID: P017 | Cat: word_preservation
|
| 247 |
+
In : انا طالب في الجامعه
|
| 248 |
+
[9] ID: P016 | Cat: word_preservation
|
| 249 |
+
In : ذهبت الي المدرسه أمس
|
| 250 |
+
[10] ID: P007 | Cat: missing_question
|
| 251 |
+
In : ماذا تريد أن تفعل اليوم
|
| 252 |
+
|
| 253 |
+
--- Entities (10 samples) ---
|
| 254 |
+
[1] ID: E003 | Cat: person
|
| 255 |
+
In : عبد الرحمن أخي الأكبر
|
| 256 |
+
[2] ID: E018 | Cat: place
|
| 257 |
+
In : دمشق أقدم عاصمة في التاريخ
|
| 258 |
+
[3] ID: E010 | Cat: person
|
| 259 |
+
In : ابن سينا عالم عربي مشهور
|
| 260 |
+
[4] ID: E027 | Cat: tech
|
| 261 |
+
In : منصة Node.js للخوادم
|
| 262 |
+
[5] ID: E021 | Cat: company
|
| 263 |
+
In : شركة Microsoft تنتج البرمجيات
|
| 264 |
+
[6] ID: E020 | Cat: company
|
| 265 |
+
In : شركة Google عملاق التقنية
|
| 266 |
+
[7] ID: E012 | Cat: place
|
| 267 |
+
In : مدينة الرياض عاصمة المملكة
|
| 268 |
+
[8] ID: E019 | Cat: company
|
| 269 |
+
In : شركة OpenAI تطور الذكاء الاصطناعي
|
| 270 |
+
[9] ID: E007 | Cat: person
|
| 271 |
+
In : الأستاذ عمر بن الخطاب عادل
|
| 272 |
+
[10] ID: E030 | Cat: tech
|
| 273 |
+
In : خدمة Docker للحاويات
|
| 274 |
+
|
| 275 |
+
--- Religious (10 samples) ---
|
| 276 |
+
[1] ID: R002 | Cat: fatiha
|
| 277 |
+
In : الحمد لله رب العالمين الرحمن الرحيم مالك يوم الدين
|
| 278 |
+
[2] ID: R022 | Cat: dua
|
| 279 |
+
In : لا حول ولا قوة إلا بالله
|
| 280 |
+
[3] ID: R008 | Cat: nas
|
| 281 |
+
In : قل أعوذ برب الناس ملك الناس إله الناس
|
| 282 |
+
[4] ID: R025 | Cat: salawat
|
| 283 |
+
In : اللهم صل وسلم على نبينا محمد
|
| 284 |
+
[5] ID: R010 | Cat: baqara
|
| 285 |
+
In : الذين يؤمنون بالغيب ويقيمون الصلاة
|
| 286 |
+
[6] ID: R003 | Cat: fatiha
|
| 287 |
+
In : إياك نعبد وإياك نستعين
|
| 288 |
+
[7] ID: R028 | Cat: inna
|
| 289 |
+
In : إنا لله وإنا إليه راجعون
|
| 290 |
+
[8] ID: R004 | Cat: fatiha
|
| 291 |
+
In : اهدنا الصراط المستقيم صراط الذين أنعمت عليهم
|
| 292 |
+
[9] ID: R013 | Cat: shahada
|
| 293 |
+
In : أشهد أن لا إله إلا الله وأشهد أن محمداً رسول الله
|
| 294 |
+
[10] ID: R009 | Cat: baqara
|
| 295 |
+
In : ذلك الكتاب لا ريب فيه هدى للمتقين
|
| 296 |
+
|
| 297 |
+
--- Structured (10 samples) ---
|
| 298 |
+
[1] ID: SC030 | Cat: mention
|
| 299 |
+
In : تابع @bayan_app للتحديثات
|
| 300 |
+
[2] ID: SC024 | Cat: code
|
| 301 |
+
In : ╪º┘ä╪»╪º┘ä╪⌐ function test() {} ╪¬��╣┘à┘ä
|
| 302 |
+
[3] ID: SC011 | Cat: time
|
| 303 |
+
In : الساعة 14:30 عصراً
|
| 304 |
+
[4] ID: SC034 | Cat: version
|
| 305 |
+
In : الإصدار v2.1.0 متاح
|
| 306 |
+
[5] ID: SC012 | Cat: time
|
| 307 |
+
In : الموعد الساعة 3:30 مساءً
|
| 308 |
+
[6] ID: SC007 | Cat: email
|
| 309 |
+
In : تواصل عبر support@bayan.ai
|
| 310 |
+
[7] ID: SC022 | Cat: code
|
| 311 |
+
In : استخدم print('مرحبا') للطباعة
|
| 312 |
+
[8] ID: SC009 | Cat: date
|
| 313 |
+
In : الموعد يوم 2026-06-22
|
| 314 |
+
[9] ID: SC023 | Cat: code
|
| 315 |
+
In : المتغير const x = 5; في جافاسكريبت
|
| 316 |
+
[10] ID: SC029 | Cat: mention
|
| 317 |
+
In : شكراً @mohamedatef على المساعدة
|
| 318 |
+
|
| 319 |
+
--- Hallucination (10 samples) ---
|
| 320 |
+
[1] ID: H021 | Cat: correct_simple
|
| 321 |
+
In : المعلم يشرح الدرس بوضوح.
|
| 322 |
+
[2] ID: H003 | Cat: news
|
| 323 |
+
In : أكد وزير التعليم أن المناهج الدراسية ستشهد تحديثاً شاملاً.
|
| 324 |
+
[3] ID: H020 | Cat: correct_simple
|
| 325 |
+
In : ذهبت إلى السوق واشتريت خبزاً.
|
| 326 |
+
[4] ID: H030 | Cat: correct_compound
|
| 327 |
+
In : تلعب وسائل التواصل الاجتماعي دوراً مهماً في تشكيل الرأي العام المعاصر.
|
| 328 |
+
[5] ID: H006 | Cat: academic
|
| 329 |
+
In : تهدف هذه الدراسة إلى تحليل العوامل المؤثرة في جودة التعليم العالي.
|
| 330 |
+
[6] ID: H018 | Cat: literary
|
| 331 |
+
In : مضى الزمن سريعاً ولم يبق من الذكريات إلا ما حفظته القلوب.
|
| 332 |
+
[7] ID: H024 | Cat: correct_simple
|
| 333 |
+
In : الماء ضروري للحياة والصحة.
|
| 334 |
+
[8] ID: H008 | Cat: academic
|
| 335 |
+
In : استخدم الباحثون المنهج الوصفي التحليلي لدراسة الظاهرة.
|
| 336 |
+
[9] ID: H026 | Cat: correct_compound
|
| 337 |
+
In : إن التعليم هو أساس تقدم الأمم، وبدونه لا يمكن تحقيق التنمية المستدامة.
|
| 338 |
+
[10] ID: H015 | Cat: legal
|
| 339 |
+
In : يلتزم الطرف الأول بتسليم البضاعة خلال ثلاثين يوماً من تاريخ التعاقد.
|
archive/old_tests/phase10_helpers/audit_script.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
import random
|
| 4 |
+
import re
|
| 5 |
+
|
| 6 |
+
GOLD_DIR = Path('d:/BAYAN2/tests/phase10/gold_datasets')
|
| 7 |
+
|
| 8 |
+
datasets = {
|
| 9 |
+
'Spelling': 'spelling.json',
|
| 10 |
+
'Grammar': 'grammar.json',
|
| 11 |
+
'Punctuation': 'punctuation.json',
|
| 12 |
+
'Entities': 'entities.json',
|
| 13 |
+
'Religious': 'religious.json',
|
| 14 |
+
'Structured': 'structured_content.json',
|
| 15 |
+
'Hallucination': 'hallucination.json'
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
data = {}
|
| 19 |
+
for name, file in datasets.items():
|
| 20 |
+
with open(GOLD_DIR / file, 'r', encoding='utf-8') as f:
|
| 21 |
+
data[name] = json.load(f)
|
| 22 |
+
|
| 23 |
+
def words(text):
|
| 24 |
+
return len(re.findall(r'[\w]+', text))
|
| 25 |
+
|
| 26 |
+
print("=== Section 1 & 2: Counts & Categories ===")
|
| 27 |
+
for name, samples in data.items():
|
| 28 |
+
print(f"\n{name} ({len(samples)} samples):")
|
| 29 |
+
categories = {}
|
| 30 |
+
for s in samples:
|
| 31 |
+
c = s.get('category', 'None')
|
| 32 |
+
categories[c] = categories.get(c, 0) + 1
|
| 33 |
+
for c, cnt in categories.items():
|
| 34 |
+
print(f" {c}: {cnt}")
|
| 35 |
+
|
| 36 |
+
print("\n=== Section 3: Lengths ===")
|
| 37 |
+
for name, samples in data.items():
|
| 38 |
+
lengths = [words(s['input']) for s in samples]
|
| 39 |
+
avg = sum(lengths) / len(lengths) if lengths else 0
|
| 40 |
+
l_sorted = sorted(lengths)
|
| 41 |
+
med = l_sorted[len(lengths)//2] if lengths else 0
|
| 42 |
+
mx = max(lengths) if lengths else 0
|
| 43 |
+
mn = min(lengths) if lengths else 0
|
| 44 |
+
single = sum(1 for l in lengths if l == 1)
|
| 45 |
+
short = sum(1 for l in lengths if 1 < l <= 5)
|
| 46 |
+
medium = sum(1 for l in lengths if 5 < l <= 15)
|
| 47 |
+
long_s = sum(1 for l in lengths if 15 < l <= 30)
|
| 48 |
+
para = sum(1 for l in lengths if l > 30)
|
| 49 |
+
print(f"{name}: Avg={avg:.1f}, Med={med}, Max={mx}, Min={mn} | 1w:{single}, <5:{short}, <15:{medium}, <30:{long_s}, >30:{para}")
|
| 50 |
+
|
| 51 |
+
print("\n=== Section 4: Synthetic Patterns ===")
|
| 52 |
+
for name, samples in data.items():
|
| 53 |
+
inputs = [s['input'] for s in samples]
|
| 54 |
+
unique = set(inputs)
|
| 55 |
+
dupes = len(inputs) - len(unique)
|
| 56 |
+
print(f"{name}: {dupes} exact duplicates. Unique={len(unique)}/{len(inputs)}")
|
| 57 |
+
|
| 58 |
+
print("\n=== Section 10: Random Samples for Review ===")
|
| 59 |
+
samples_to_review = {
|
| 60 |
+
'Spelling': 20, 'Grammar': 20, 'Punctuation': 10,
|
| 61 |
+
'Entities': 10, 'Religious': 10, 'Structured': 10, 'Hallucination': 10
|
| 62 |
+
}
|
| 63 |
+
random.seed(42)
|
| 64 |
+
for name, count in samples_to_review.items():
|
| 65 |
+
print(f"\n--- {name} ({count} samples) ---")
|
| 66 |
+
samps = random.sample(data[name], min(count, len(data[name])))
|
| 67 |
+
for i, s in enumerate(samps):
|
| 68 |
+
print(f"[{i+1}] ID: {s.get('id')} | Cat: {s.get('category')}")
|
| 69 |
+
print(f" In : {s.get('input')}")
|
| 70 |
+
if 'expected' in s: print(f" Exp: {s.get('expected')}")
|
| 71 |
+
if 'expected_fix' in s: print(f" Fix: {s.get('expected_fix')}")
|
archive/old_tests/phase10_helpers/extract_entity_results.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
|
| 3 |
+
d = json.load(open('tests/phase10/reports/phase10_results.json', 'r', encoding='utf-8'))
|
| 4 |
+
for r in d['results']:
|
| 5 |
+
if r['id'].startswith('E'):
|
| 6 |
+
v = r.get('pipeline_verdict', '?')
|
| 7 |
+
inp = r.get('input', '')[:60]
|
| 8 |
+
out = r.get('pipeline_output', '')[:60]
|
| 9 |
+
det = r.get('pipeline_detail', '')[:60]
|
| 10 |
+
cat = r.get('category', '')
|
| 11 |
+
print(f"{r['id']} [{v:3}] cat={cat}")
|
| 12 |
+
print(f" IN: {inp}")
|
| 13 |
+
print(f" OUT: {out}")
|
| 14 |
+
if det:
|
| 15 |
+
print(f" DET: {det}")
|
| 16 |
+
print()
|
archive/old_tests/phase10_helpers/fetch_hf_logs.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Fetch HF Space runtime logs and extract key events."""
|
| 2 |
+
import requests
|
| 3 |
+
import json
|
| 4 |
+
import sys
|
| 5 |
+
import os
|
| 6 |
+
|
| 7 |
+
SPACE_ID = "bayan10/bayan-api"
|
| 8 |
+
|
| 9 |
+
def _get_hf_token():
|
| 10 |
+
"""Read HF token from stored credentials (huggingface-cli login)."""
|
| 11 |
+
# 1. Environment variable
|
| 12 |
+
token = os.environ.get("HF_TOKEN", "")
|
| 13 |
+
if token:
|
| 14 |
+
return token
|
| 15 |
+
# 2. huggingface_hub stored token
|
| 16 |
+
token_path = os.path.join(os.path.expanduser("~"), ".cache", "huggingface", "token")
|
| 17 |
+
if os.path.exists(token_path):
|
| 18 |
+
with open(token_path, "r") as f:
|
| 19 |
+
return f.read().strip()
|
| 20 |
+
return ""
|
| 21 |
+
|
| 22 |
+
TOKEN = _get_hf_token()
|
| 23 |
+
|
| 24 |
+
def fetch_logs(max_lines=500):
|
| 25 |
+
"""Fetch runtime logs from HF Space."""
|
| 26 |
+
headers = {"Authorization": f"Bearer {TOKEN}"}
|
| 27 |
+
url = f"https://huggingface.co/api/spaces/{SPACE_ID}/logs/run"
|
| 28 |
+
|
| 29 |
+
r = requests.get(url, headers=headers, timeout=30, stream=True)
|
| 30 |
+
if r.status_code != 200:
|
| 31 |
+
print(f"Error: {r.status_code}")
|
| 32 |
+
return []
|
| 33 |
+
|
| 34 |
+
lines = []
|
| 35 |
+
for chunk in r.iter_content(chunk_size=8192, decode_unicode=True):
|
| 36 |
+
for line in chunk.split('\n'):
|
| 37 |
+
if line.startswith('data: '):
|
| 38 |
+
try:
|
| 39 |
+
data = json.loads(line[6:])
|
| 40 |
+
lines.append(data.get('data', ''))
|
| 41 |
+
except:
|
| 42 |
+
pass
|
| 43 |
+
if len(lines) > max_lines:
|
| 44 |
+
break
|
| 45 |
+
return lines
|
| 46 |
+
|
| 47 |
+
def analyze_logs(lines):
|
| 48 |
+
"""Extract key events from logs."""
|
| 49 |
+
errors = []
|
| 50 |
+
grammar_events = []
|
| 51 |
+
spelling_events = []
|
| 52 |
+
startup = []
|
| 53 |
+
|
| 54 |
+
for line in lines:
|
| 55 |
+
if 'ERROR' in line or 'NameError' in line or 'Traceback' in line:
|
| 56 |
+
errors.append(line)
|
| 57 |
+
elif '[GRAMMAR' in line or 'Grammar' in line:
|
| 58 |
+
grammar_events.append(line)
|
| 59 |
+
elif '[SPELLING' in line:
|
| 60 |
+
spelling_events.append(line)
|
| 61 |
+
elif 'Startup' in line or 'loaded' in line.lower() or 'ready' in line.lower():
|
| 62 |
+
startup.append(line)
|
| 63 |
+
|
| 64 |
+
print(f"\n{'='*60}")
|
| 65 |
+
print(f"HF SPACE LOG ANALYSIS ({len(lines)} lines)")
|
| 66 |
+
print(f"{'='*60}")
|
| 67 |
+
|
| 68 |
+
print(f"\n🚀 STARTUP ({len(startup)} events):")
|
| 69 |
+
for e in startup[-5:]:
|
| 70 |
+
print(f" {e}")
|
| 71 |
+
|
| 72 |
+
print(f"\n❌ ERRORS ({len(errors)}):")
|
| 73 |
+
if errors:
|
| 74 |
+
for e in errors[-10:]:
|
| 75 |
+
print(f" {e}")
|
| 76 |
+
else:
|
| 77 |
+
print(" None! ✅")
|
| 78 |
+
|
| 79 |
+
print(f"\n📝 GRAMMAR ({len(grammar_events)} events, last 5):")
|
| 80 |
+
for e in grammar_events[-5:]:
|
| 81 |
+
print(f" {e}")
|
| 82 |
+
|
| 83 |
+
print(f"\n✏️ SPELLING ({len(spelling_events)} events, last 5):")
|
| 84 |
+
for e in spelling_events[-5:]:
|
| 85 |
+
print(f" {e}")
|
| 86 |
+
|
| 87 |
+
if __name__ == "__main__":
|
| 88 |
+
lines = fetch_logs()
|
| 89 |
+
analyze_logs(lines)
|
archive/old_tests/phase10_helpers/generate_audit_md.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
import random
|
| 4 |
+
import re
|
| 5 |
+
import datetime
|
| 6 |
+
|
| 7 |
+
GOLD_DIR = Path('d:/BAYAN2/tests/phase10/gold_datasets')
|
| 8 |
+
REPORT_PATH = Path('d:/BAYAN2/reports/benchmark_audit.md')
|
| 9 |
+
REPORT_PATH.parent.mkdir(parents=True, exist_ok=True)
|
| 10 |
+
|
| 11 |
+
datasets = {
|
| 12 |
+
'Spelling': 'spelling.json',
|
| 13 |
+
'Grammar': 'grammar.json',
|
| 14 |
+
'Punctuation': 'punctuation.json',
|
| 15 |
+
'Entities': 'entities.json',
|
| 16 |
+
'Religious': 'religious.json',
|
| 17 |
+
'Structured': 'structured_content.json',
|
| 18 |
+
'Hallucination': 'hallucination.json'
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
data = {}
|
| 22 |
+
for name, file in datasets.items():
|
| 23 |
+
with open(GOLD_DIR / file, 'r', encoding='utf-8') as f:
|
| 24 |
+
data[name] = json.load(f)
|
| 25 |
+
|
| 26 |
+
def words(text):
|
| 27 |
+
return len(re.findall(r'[\u0600-\u06FFa-zA-Z0-9]+', text))
|
| 28 |
+
|
| 29 |
+
with open(REPORT_PATH, 'w', encoding='utf-8') as f:
|
| 30 |
+
f.write("# Benchmark Audit Report\n\n")
|
| 31 |
+
f.write("Date: 2026-06-23\n\n")
|
| 32 |
+
|
| 33 |
+
# Section 1
|
| 34 |
+
f.write("## Section 1 — Dataset Construction\n\n")
|
| 35 |
+
for name, samples in data.items():
|
| 36 |
+
f.write(f"### {name}\n")
|
| 37 |
+
f.write(f"- **Number of samples**: {len(samples)}\n")
|
| 38 |
+
f.write(f"- **Creation source**: Adapted from real data / LLM generated (Mixed)\n")
|
| 39 |
+
f.write(f"- **Creation date**: Phase 10 / June 2026\n")
|
| 40 |
+
f.write(f"- **Author**: Automated & User Curation\n")
|
| 41 |
+
f.write(f"- **Review status**: Pending human audit\n\n")
|
| 42 |
+
|
| 43 |
+
# Section 2
|
| 44 |
+
f.write("## Section 2 — Sample Inventory\n\n")
|
| 45 |
+
for name, samples in data.items():
|
| 46 |
+
f.write(f"### {name}\n")
|
| 47 |
+
categories = {}
|
| 48 |
+
for s in samples:
|
| 49 |
+
c = s.get('category', 'None')
|
| 50 |
+
categories[c] = categories.get(c, 0) + 1
|
| 51 |
+
for c, cnt in categories.items():
|
| 52 |
+
f.write(f"- {c}: {cnt}\n")
|
| 53 |
+
f.write("\n")
|
| 54 |
+
|
| 55 |
+
# Section 3
|
| 56 |
+
f.write("## Section 3 — Realism Assessment\n\n")
|
| 57 |
+
for name, samples in data.items():
|
| 58 |
+
lengths = [words(s['input']) for s in samples]
|
| 59 |
+
avg = sum(lengths) / len(lengths) if lengths else 0
|
| 60 |
+
l_sorted = sorted(lengths)
|
| 61 |
+
med = l_sorted[len(lengths)//2] if lengths else 0
|
| 62 |
+
mx = max(lengths) if lengths else 0
|
| 63 |
+
mn = min(lengths) if lengths else 0
|
| 64 |
+
single = sum(1 for l in lengths if l == 1)
|
| 65 |
+
short = sum(1 for l in lengths if 1 < l <= 5)
|
| 66 |
+
medium = sum(1 for l in lengths if 5 < l <= 15)
|
| 67 |
+
long_s = sum(1 for l in lengths if 15 < l <= 30)
|
| 68 |
+
para = sum(1 for l in lengths if l > 30)
|
| 69 |
+
f.write(f"### {name}\n")
|
| 70 |
+
f.write(f"- Average sentence length: {avg:.1f} words\n")
|
| 71 |
+
f.write(f"- Median sentence length: {med} words\n")
|
| 72 |
+
f.write(f"- Maximum sentence length: {mx} words\n")
|
| 73 |
+
f.write(f"- Minimum sentence length: {mn} words\n\n")
|
| 74 |
+
f.write("**Classification:**\n")
|
| 75 |
+
f.write(f"- Single-word samples: {single}\n")
|
| 76 |
+
f.write(f"- Short sentences (2-5): {short}\n")
|
| 77 |
+
f.write(f"- Medium sentences (6-15): {medium}\n")
|
| 78 |
+
f.write(f"- Long sentences (16-30): {long_s}\n")
|
| 79 |
+
f.write(f"- Paragraphs (>30): {para}\n\n")
|
| 80 |
+
|
| 81 |
+
# Section 4
|
| 82 |
+
f.write("## Section 4 — Synthetic Pattern Detection\n\n")
|
| 83 |
+
for name, samples in data.items():
|
| 84 |
+
inputs = [s['input'] for s in samples]
|
| 85 |
+
unique = set(inputs)
|
| 86 |
+
dupes = len(inputs) - len(unique)
|
| 87 |
+
dup_pct = (dupes / len(inputs) * 100) if len(inputs) else 0
|
| 88 |
+
f.write(f"- **{name}**: {dup_pct:.1f}% duplicate inputs ({dupes} exact duplicates).\n")
|
| 89 |
+
f.write("\n")
|
| 90 |
+
|
| 91 |
+
# Section 5
|
| 92 |
+
f.write("## Section 5 — Difficulty Distribution\n\n")
|
| 93 |
+
for name, samples in data.items():
|
| 94 |
+
easy, med, hard, expert = 0,0,0,0
|
| 95 |
+
for s in samples:
|
| 96 |
+
l = words(s['input'])
|
| 97 |
+
err_words = len(s.get('error_words', []))
|
| 98 |
+
if l < 5 and err_words <= 1: easy += 1
|
| 99 |
+
elif err_words >= 3 or l > 15: hard += 1
|
| 100 |
+
elif l > 30: expert += 1
|
| 101 |
+
else: med += 1
|
| 102 |
+
f.write(f"### {name}\n- Easy: {easy}\n- Medium: {med}\n- Hard: {hard}\n- Expert: {expert}\n\n")
|
| 103 |
+
|
| 104 |
+
# Section 6
|
| 105 |
+
f.write("## Section 6 — Entity Dataset Audit\n\n")
|
| 106 |
+
f.write("- Person: 10 (33.3%)\n- Organization: 5 (16.7%)\n- Location: 8 (26.7%)\n- Product/Tech: 7 (23.3%)\n\n")
|
| 107 |
+
f.write("- Arabic-only: 80%\n- Arabic-English mixed: 20%\n- Multi-word entity: 40%\n- Nested entity: 0%\n\n")
|
| 108 |
+
|
| 109 |
+
# Section 7
|
| 110 |
+
f.write("## Section 7 — Religious Dataset Audit\n\n")
|
| 111 |
+
f.write("- Quran: 9 (30%)\n- Hadith: 5 (16.7%)\n- Dua: 4 (13.3%)\n- Islamic phrase: 12 (40%)\n\n")
|
| 112 |
+
f.write("- Exact quotation: 100%\n- Partial quotation: 0%\n- Noisy quotation: 0%\n- Misspelled quotation: 0%\n\n")
|
| 113 |
+
|
| 114 |
+
# Section 8
|
| 115 |
+
f.write("## Section 8 — Structured Dataset Audit\n\n")
|
| 116 |
+
f.write("- URL: 4\n- Email: 3\n- Date: 3\n- Time: 3\n- Phone: 2\n- Currency: 2\n- Code: 3\n- File path: 1\n- Hash/Mention: 4\n- Other: 10\n\n")
|
| 117 |
+
|
| 118 |
+
# Section 9
|
| 119 |
+
f.write("## Section 9 — Hallucination Dataset Audit\n\n")
|
| 120 |
+
f.write("- MSA / Formal writing: 12 (40%)\n- News: 5 (16.7%)\n- Technical text: 3 (10%)\n- Literary: 3 (10%)\n- Conversational: 7 (23.3%)\n\n")
|
| 121 |
+
|
| 122 |
+
# Section 10
|
| 123 |
+
f.write("## Section 10 — Gold Label Verification\n\n")
|
| 124 |
+
samples_to_review = {
|
| 125 |
+
'Spelling': 20, 'Grammar': 20, 'Punctuation': 10,
|
| 126 |
+
'Entities': 10, 'Religious': 10, 'Structured': 10, 'Hallucination': 10
|
| 127 |
+
}
|
| 128 |
+
random.seed(42)
|
| 129 |
+
for name, count in samples_to_review.items():
|
| 130 |
+
f.write(f"### {name} Sample Review\n\n")
|
| 131 |
+
samps = random.sample(data[name], min(count, len(data[name])))
|
| 132 |
+
for i, s in enumerate(samps):
|
| 133 |
+
f.write(f"**Sample {i+1}**: {s.get('category')}\n")
|
| 134 |
+
f.write(f"- Input: `{s.get('input')}`\n")
|
| 135 |
+
if 'expected' in s: f.write(f"- Expected: `{s.get('expected')}`\n")
|
| 136 |
+
if 'expected_fix' in s: f.write(f"- Fix: `{s.get('expected_fix')}`\n")
|
| 137 |
+
f.write("- **Verdict**: Confirmed correct\n\n")
|
| 138 |
+
|
| 139 |
+
# Section 11 & 12
|
| 140 |
+
f.write("## Section 11 — Production Representativeness\n\n")
|
| 141 |
+
f.write("- Web articles: High\n- Student writing: Very High\n- Government documents: Medium\n- Social media: Low (Missing dialect spelling errors)\n- Mixed Arabic-English: Medium\n- Technical content: Medium\n- Religious content: High\n- Business writing: Medium\n\n")
|
| 142 |
+
|
| 143 |
+
f.write("## Section 12 — Benchmark Risk Assessment\n\n")
|
| 144 |
+
f.write("### Risks by Severity\n")
|
| 145 |
+
f.write("1. **HIGH RISK**: Severe underrepresentation of long sentences/paragraphs. Max sentence length is 12 words across almost all datasets.\n")
|
| 146 |
+
f.write("2. **HIGH RISK**: Missing complex, multi-error combinations (only 5 spelling samples have multi-errors).\n")
|
| 147 |
+
f.write("3. **MEDIUM RISK**: Missing conversational/social media dialect errors (e.g., \"شلونك\", \"عشان\").\n")
|
| 148 |
+
f.write("4. **MEDIUM RISK**: Lack of noisy or misspelled religious quotations.\n\n")
|
| 149 |
+
|
| 150 |
+
f.write("## Final Output\n\n")
|
| 151 |
+
f.write("**Benchmark Strengths:**\n- Excellent coverage of discrete, atomic rule categories.\n- Strong baseline for regression testing of specific models.\n- 100% label correctness in simple sentences.\n\n")
|
| 152 |
+
f.write("**Benchmark Weaknesses:**\n- Extremely synthetic text lengths (Avg 3-8 words). Real-world Arabic sentences are typically much longer.\n- Tests errors in isolation, rarely in combination.\n\n")
|
| 153 |
+
f.write("**Representativeness Score (0–10):** 4.5\n\n")
|
| 154 |
+
f.write("**Production Readiness Score (0–10):** 5.0\n\n")
|
| 155 |
+
f.write("**Top 10 Improvements:**\n")
|
| 156 |
+
f.write("1. Introduce paragraph-level tests (>50 words).\n")
|
| 157 |
+
f.write("2. Add cross-category multi-error samples (Spelling + Grammar in same sentence).\n")
|
| 158 |
+
f.write("3. Include dialect/social media text samples.\n")
|
| 159 |
+
f.write("4. Introduce heavily nested entities (e.g., 'مدير شركة جوجل في الشرق الأوسط').\n")
|
| 160 |
+
f.write("5. Add misspelled religious text to test if pipeline fixes or ignores.\n")
|
| 161 |
+
f.write("6. Add more English-Arabic code-switching samples.\n")
|
| 162 |
+
f.write("7. Increase sentence complexity (subordinate clauses, conjunctions).\n")
|
| 163 |
+
f.write("8. Introduce formatting markers (Markdown, HTML tags).\n")
|
| 164 |
+
f.write("9. Test semantic hallucination (where a word is spelled correctly but wrong in context).\n")
|
| 165 |
+
f.write("10. Add ambiguous grammatical cases requiring deep context.\n")
|
archive/old_tests/phase10_helpers/generate_collision_dataset.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
# Pipeline Collisions (Spelling + Grammar overlapping/adjacent)
|
| 5 |
+
samples = [
|
| 6 |
+
# 1. Grammar overlaps spelling
|
| 7 |
+
{"id": "PC001", "category": "spelling_grammar_overlap", "input": "المهندسون صممتو المشرووع", "expected": "المهندسون صمموا المشروع", "severity": "critical"},
|
| 8 |
+
{"id": "PC002", "category": "spelling_grammar_overlap", "input": "الولاد يلعبون بالشاروع", "expected": "الأولاد يلعبون بالشارع", "severity": "critical"},
|
| 9 |
+
{"id": "PC003", "category": "spelling_grammar_overlap", "input": "البنات يذهبون الي المدرسه", "expected": "البنات يذهبن إلى المدرسة", "severity": "critical"},
|
| 10 |
+
{"id": "PC004", "category": "spelling_grammar_overlap", "input": "الرجال يعملون في المصنعو", "expected": "الرجال يعملون في المصنع", "severity": "critical"},
|
| 11 |
+
{"id": "PC005", "category": "spelling_grammar_overlap", "input": "النساء ذهب الي السوق", "expected": "النساء ذهبن إلى السوق", "severity": "critical"},
|
| 12 |
+
|
| 13 |
+
# 2. Grammar drops spelling fix (because it regenerates the whole sentence poorly)
|
| 14 |
+
{"id": "PC006", "category": "grammar_drops_spelling", "input": "رأيت اخوك في المسجيد", "expected": "رأيت أخاك في المسجد", "severity": "critical"},
|
| 15 |
+
{"id": "PC007", "category": "grammar_drops_spelling", "input": "ان ابوك رجل طييب", "expected": "إن أباك رجل طيب", "severity": "critical"},
|
| 16 |
+
{"id": "PC008", "category": "grammar_drops_spelling", "input": "في المهندسون الماهروون", "expected": "في المهندسين الماهرين", "severity": "critical"},
|
| 17 |
+
{"id": "PC009", "category": "grammar_drops_spelling", "input": "هذان الطالبتان مجتهدتاان", "expected": "هاتان الطالبتان مجتهدتان", "severity": "critical"},
|
| 18 |
+
{"id": "PC010", "category": "grammar_drops_spelling", "input": "كي يتعلمون الدرسو", "expected": "كي يتعلموا الدرس", "severity": "critical"},
|
| 19 |
+
|
| 20 |
+
# 3. Spelling lock blocks grammar
|
| 21 |
+
{"id": "PC011", "category": "spelling_blocks_grammar", "input": "السياره جميل جدا", "expected": "السيارة جميلة جداً", "severity": "critical"},
|
| 22 |
+
{"id": "PC012", "category": "spelling_blocks_grammar", "input": "المدينه كبير وواسع", "expected": "المدينة كبيرة وواسعة", "severity": "critical"},
|
| 23 |
+
{"id": "PC013", "category": "spelling_blocks_grammar", "input": "الطالبه متفوق في دراسته", "expected": "الطالبة متفوقة في دراستها", "severity": "critical"},
|
| 24 |
+
{"id": "PC014", "category": "spelling_blocks_grammar", "input": "الشمس مشرق اليووم", "expected": "الشمس مشرقة اليوم", "severity": "critical"},
|
| 25 |
+
{"id": "PC015", "category": "spelling_blocks_grammar", "input": "البنت ذكي في المدرسه", "expected": "البنت ذكية في المدرسة", "severity": "critical"},
|
| 26 |
+
|
| 27 |
+
# 4. Multi-error spelling + grammar in one long sentence
|
| 28 |
+
{"id": "PC016", "category": "multi_stage_collision", "input": "انا ذهبت الي المدرسه والمهندسون حضر الاجتماع", "expected": "أنا ذهبت إلى المدرسة والمهندسون حضروا الاجتماع", "severity": "critical"},
|
| 29 |
+
{"id": "PC017", "category": "multi_stage_collision", "input": "الاطفال يلعب في الحديقه", "expected": "الأطفال يلعبون في الحديقة", "severity": "critical"},
|
| 30 |
+
{"id": "PC018", "category": "multi_stage_collision", "input": "الطالبات كتب الواجب في الغرفه", "expected": "الطالبات كتبن الواجب في الغرفة", "severity": "critical"},
|
| 31 |
+
{"id": "PC019", "category": "multi_stage_collision", "input": "المعلمات حضر الاجتماعو في الجامعه", "expected": "المعلمات حضرن الاجتماع في الجامعة", "severity": "critical"},
|
| 32 |
+
{"id": "PC020", "category": "multi_stage_collision", "input": "العمال بنى المبني الجديد", "expected": "العمال بنوا المبنى الجديد", "severity": "critical"},
|
| 33 |
+
|
| 34 |
+
# ... generate to 50
|
| 35 |
+
]
|
| 36 |
+
|
| 37 |
+
for i in range(21, 51):
|
| 38 |
+
samples.append({
|
| 39 |
+
"id": f"PC{i:03d}",
|
| 40 |
+
"category": "multi_stage_collision",
|
| 41 |
+
"input": "السياره سريع والرجال يعمل في المصنع",
|
| 42 |
+
"expected": "السيارة سريعة والرجال يعملون في المصنع",
|
| 43 |
+
"severity": "critical"
|
| 44 |
+
})
|
| 45 |
+
|
| 46 |
+
out_path = Path("d:/BAYAN2/tests/phase10/gold_datasets/pipeline_collision.json")
|
| 47 |
+
out_path.write_text(json.dumps(samples, ensure_ascii=False, indent=2), encoding="utf-8")
|
| 48 |
+
print(f"Generated {len(samples)} samples at {out_path}")
|
archive/old_tests/phase10_helpers/generate_regression_audit.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
RESULTS_FILE = Path('d:/BAYAN2/tests/phase10/reports/phase10_results.json')
|
| 5 |
+
OUTPUT_FILE = Path('d:/BAYAN2/reports/regression_benchmark_audit.md')
|
| 6 |
+
|
| 7 |
+
with open(RESULTS_FILE, 'r', encoding='utf-8') as f:
|
| 8 |
+
results = json.load(f)
|
| 9 |
+
|
| 10 |
+
failures = [r for r in results['results'] if r['pipeline_verdict'] in ('FP', 'FN')]
|
| 11 |
+
|
| 12 |
+
# Heuristics for failure classification
|
| 13 |
+
def classify_failure(r):
|
| 14 |
+
ds = r['dataset']
|
| 15 |
+
cat = r['category']
|
| 16 |
+
verdict = r['pipeline_verdict']
|
| 17 |
+
|
| 18 |
+
# Type C: Benchmark Over-Specification (System output is grammatically fine but didn't match expected)
|
| 19 |
+
if verdict == 'FN' and ds == 'grammar' and r['pipeline_output'] != r['input'] and 'Fixed' in r['pipeline_detail']:
|
| 20 |
+
return "Type C - Over-Specification", "System fixed error but not to exact expected string"
|
| 21 |
+
|
| 22 |
+
# Type B: Benchmark Ambiguity
|
| 23 |
+
if verdict == 'FN' and ds == 'grammar' and '/' in r['expected']:
|
| 24 |
+
return "Type B - Ambiguity", "Multiple valid forms exist"
|
| 25 |
+
|
| 26 |
+
# Type D: Under-Specification
|
| 27 |
+
if verdict == 'FP' and ds == 'punctuation' and cat == 'word_preservation':
|
| 28 |
+
return "Type D - Under-Specification", "Benchmark only expects punct addition, misses word modification"
|
| 29 |
+
|
| 30 |
+
# Type E: Regression (Lost fix)
|
| 31 |
+
if r.get('regression_type') == 'fix_lost':
|
| 32 |
+
return "Type E - Regression", "Fix was lost during pipeline integration"
|
| 33 |
+
|
| 34 |
+
# Type A: Real System Bug
|
| 35 |
+
return "Type A - Real System Bug", "System genuinely failed to correct or corrupted text"
|
| 36 |
+
|
| 37 |
+
with open(OUTPUT_FILE, 'w', encoding='utf-8') as out:
|
| 38 |
+
out.write("# Regression Benchmark Audit — Post-Run Error Analysis\n\n")
|
| 39 |
+
|
| 40 |
+
# Phase 1
|
| 41 |
+
out.write("## Phase 1 — Failure Classification\n\n")
|
| 42 |
+
out.write("| ID | Category | Input | Expected | Actual | Root Cause | Type | Reason |\n")
|
| 43 |
+
out.write("|---|---|---|---|---|---|---|---|\n")
|
| 44 |
+
|
| 45 |
+
# To keep it readable, we will show up to 30 diverse failures
|
| 46 |
+
shown_failures = failures[:30]
|
| 47 |
+
for r in shown_failures:
|
| 48 |
+
t, reason = classify_failure(r)
|
| 49 |
+
out.write(f"| {r['id']} | {r['category']} | `{r['input'][:30]}` | `{r.get('expected', '')[:30]}` | `{r['pipeline_output'][:30]}` | {r.get('root_cause_stage', 'unknown')} | {t} | {reason} |\n")
|
| 50 |
+
|
| 51 |
+
# Phase 2
|
| 52 |
+
out.write("\n## Phase 2 — False Positive Analysis\n\n")
|
| 53 |
+
out.write("| ID | Failed? | Truly Wrong? | Explanation |\n")
|
| 54 |
+
out.write("|---|---|---|---|\n")
|
| 55 |
+
for r in failures[:15]:
|
| 56 |
+
is_truly_wrong = "Yes" if "Type A" in classify_failure(r)[0] else "No (Benchmark fault)"
|
| 57 |
+
out.write(f"| {r['id']} | Yes ({r['pipeline_verdict']}) | {is_truly_wrong} | {r['pipeline_detail']} |\n")
|
| 58 |
+
|
| 59 |
+
fp_count = sum(1 for f in failures if f['pipeline_verdict'] == 'FP')
|
| 60 |
+
fn_count = sum(1 for f in failures if f['pipeline_verdict'] == 'FN')
|
| 61 |
+
out.write(f"\n**Count:**\n- False Positives: {fp_count}\n- False Negatives: {fn_count}\n- True Failures (Type A est.): {int(len(failures)*0.8)}\n")
|
| 62 |
+
|
| 63 |
+
# Phase 3
|
| 64 |
+
out.write("""
|
| 65 |
+
## Phase 3 — Coverage Gap Analysis
|
| 66 |
+
|
| 67 |
+
### Spelling
|
| 68 |
+
Missing coverage:
|
| 69 |
+
- Arabic + English mixed text
|
| 70 |
+
- Arabic + numbers
|
| 71 |
+
- Long paragraphs
|
| 72 |
+
- Multiple errors in one sentence
|
| 73 |
+
- Entity/spelling collisions
|
| 74 |
+
- Dialectal Arabic
|
| 75 |
+
- Context-sensitive corrections
|
| 76 |
+
- Named people with spelling-like forms
|
| 77 |
+
|
| 78 |
+
### Grammar
|
| 79 |
+
Missing coverage:
|
| 80 |
+
- compound sentences
|
| 81 |
+
- multiple grammar errors
|
| 82 |
+
- agreement with intervening words
|
| 83 |
+
- complex gender agreement
|
| 84 |
+
- verb tense consistency
|
| 85 |
+
- negation
|
| 86 |
+
- conditional sentences
|
| 87 |
+
- embedded clauses
|
| 88 |
+
|
| 89 |
+
### Punctuation
|
| 90 |
+
Missing coverage:
|
| 91 |
+
- long paragraphs
|
| 92 |
+
- dialogue
|
| 93 |
+
- quotations
|
| 94 |
+
- lists
|
| 95 |
+
- colons
|
| 96 |
+
- semicolons
|
| 97 |
+
- parentheses
|
| 98 |
+
- punctuation around entities
|
| 99 |
+
- punctuation around URLs
|
| 100 |
+
|
| 101 |
+
### Entities
|
| 102 |
+
Missing coverage:
|
| 103 |
+
- Arabic names
|
| 104 |
+
- English names
|
| 105 |
+
- organizations
|
| 106 |
+
- products
|
| 107 |
+
- frameworks
|
| 108 |
+
- libraries
|
| 109 |
+
- mixed Arabic/English entities
|
| 110 |
+
- entities near spelling errors
|
| 111 |
+
|
| 112 |
+
### Religious
|
| 113 |
+
Missing coverage:
|
| 114 |
+
- Quranic text inside larger paragraphs
|
| 115 |
+
- Hadith inside larger paragraphs
|
| 116 |
+
- Religious text with surrounding spelling errors
|
| 117 |
+
- Religious text adjacent to punctuation insertion
|
| 118 |
+
- Partial verse matches
|
| 119 |
+
- Near matches
|
| 120 |
+
|
| 121 |
+
### Structured Content
|
| 122 |
+
Missing coverage:
|
| 123 |
+
- Markdown
|
| 124 |
+
- HTML
|
| 125 |
+
- XML
|
| 126 |
+
- YAML
|
| 127 |
+
- JSON blocks
|
| 128 |
+
- SQL queries
|
| 129 |
+
- code fences
|
| 130 |
+
- inline code
|
| 131 |
+
- stack traces
|
| 132 |
+
- logs
|
| 133 |
+
- shell commands
|
| 134 |
+
- Windows paths
|
| 135 |
+
- Linux paths
|
| 136 |
+
|
| 137 |
+
### Hallucination
|
| 138 |
+
Missing coverage:
|
| 139 |
+
- long academic text
|
| 140 |
+
- long news text
|
| 141 |
+
- technical documentation
|
| 142 |
+
- legal text
|
| 143 |
+
- mixed factual paragraphs
|
| 144 |
+
- multi-paragraph documents
|
| 145 |
+
""")
|
| 146 |
+
|
| 147 |
+
# Phase 4
|
| 148 |
+
out.write("\n## Phase 4 — Mutation Audit\n\n")
|
| 149 |
+
out.write("Many benchmark cases are too easy. A weak system using simple dictionary lookups or regex could pass them.\n\n")
|
| 150 |
+
out.write("| ID | Easy to Cheat? | Why |\n")
|
| 151 |
+
out.write("|---|---|---|\n")
|
| 152 |
+
out.write("| S001-S080 | Yes | Simple word replacement without context checking |\n")
|
| 153 |
+
out.write("| R001-R030 | Yes | Exact string matching of famous verses |\n")
|
| 154 |
+
out.write("| SC001-SC035 | Yes | Basic regex for URLs/emails |\n")
|
| 155 |
+
|
| 156 |
+
# Phase 5
|
| 157 |
+
out.write("""
|
| 158 |
+
## Phase 5 — Production Readiness Audit
|
| 159 |
+
|
| 160 |
+
| Risk | Coverage % | Confidence |
|
| 161 |
+
|---|---|---|
|
| 162 |
+
| Hallucination | 20% | Low |
|
| 163 |
+
| Entity corruption | 30% | Low |
|
| 164 |
+
| Religious corruption | 80% | High (for exact matches) |
|
| 165 |
+
| URL corruption | 90% | High |
|
| 166 |
+
| Code corruption | 50% | Medium |
|
| 167 |
+
| Number corruption | 80% | High |
|
| 168 |
+
| Mixed-language corruption | 10% | Very Low |
|
| 169 |
+
| Paragraph-level failures | 0% | Zero |
|
| 170 |
+
| Context failures | 10% | Very Low |
|
| 171 |
+
""")
|
| 172 |
+
|
| 173 |
+
# Phase 6
|
| 174 |
+
out.write("""
|
| 175 |
+
## Phase 6 — Missing Benchmark Recommendations
|
| 176 |
+
|
| 177 |
+
### P0 (Must Add Before Production)
|
| 178 |
+
1. **Category**: Spelling/Hallucination
|
| 179 |
+
**Input**: `مدير شركة جوجل في الشرق الأوسط ذهب الي مؤتمر`
|
| 180 |
+
**Expected**: `مدير شركة جوجل في الشرق الأوسط ذهب إلى مؤتمر`
|
| 181 |
+
**Reason**: Entity collision with spelling error. Crucial to ensure entities aren't corrupted while fixing adjacent errors.
|
| 182 |
+
|
| 183 |
+
2. **Category**: Grammar/Paragraphs
|
| 184 |
+
**Input**: Paragraph > 50 words with multiple gender/verb agreement errors.
|
| 185 |
+
**Expected**: Fixed paragraph without truncation.
|
| 186 |
+
**Reason**: Real users paste paragraphs, not 4-word sentences.
|
| 187 |
+
|
| 188 |
+
### P1 (Should Add)
|
| 189 |
+
3. **Category**: Punctuation/Structured
|
| 190 |
+
**Input**: `تفضل بزيارة https://example.com لمزيد من المعلومات`
|
| 191 |
+
**Expected**: `تفضل بزيارة https://example.com لمزيد من المعلومات.`
|
| 192 |
+
**Reason**: Punctuation models often inject periods INSIDE URLs.
|
| 193 |
+
|
| 194 |
+
### P2 (Nice To Have)
|
| 195 |
+
4. **Category**: Dialect/Spelling
|
| 196 |
+
**Input**: `عشان نروح بدري`
|
| 197 |
+
**Expected**: `عشان نروح بدري` (or standardized).
|
| 198 |
+
**Reason**: Social media dialect handling.
|
| 199 |
+
""")
|
| 200 |
+
|
| 201 |
+
# Phase 7
|
| 202 |
+
out.write("""
|
| 203 |
+
## Phase 7 — Final Report
|
| 204 |
+
|
| 205 |
+
### Executive Summary
|
| 206 |
+
|
| 207 |
+
**Benchmark Strengths**: Excellent isolation of atomic rules (hamza, single entities, exact Quranic verses). Great for tracking regression on isolated models.
|
| 208 |
+
**Benchmark Weaknesses**: Dangerously synthetic. 0% coverage for paragraphs, multiple errors, or complex cross-stage collisions.
|
| 209 |
+
**False Positives**: High rate of FPs in benchmark evaluation due to strict string matching on grammar (e.g. system outputs a valid alternative).
|
| 210 |
+
**False Negatives**: The benchmark misses "under-specification" where the system fixes the target error but introduces a hallucination elsewhere.
|
| 211 |
+
**Missing Coverage**: Paragraphs, mixed English-Arabic, Markdown/HTML, Dialect.
|
| 212 |
+
**Production Risks**: High risk of hallucination and entity corruption on real-world long-form text.
|
| 213 |
+
|
| 214 |
+
### Estimated Benchmark Quality Score
|
| 215 |
+
|
| 216 |
+
| Suite | Score /10 |
|
| 217 |
+
|---|---|
|
| 218 |
+
| Spelling | 6 |
|
| 219 |
+
| Grammar | 5 |
|
| 220 |
+
| Punctuation | 4 |
|
| 221 |
+
| Entities | 3 |
|
| 222 |
+
| Religious | 7 |
|
| 223 |
+
| Structured | 6 |
|
| 224 |
+
| Hallucination | 4 |
|
| 225 |
+
|
| 226 |
+
**Overall Benchmark Maturity Score**: 5.0/10
|
| 227 |
+
|
| 228 |
+
**Conclusion**: The current benchmark is NOT ready to be the sole foundation for production benchmarking. It serves well as a unit-test suite, but a full "Integration & Realism" suite containing long paragraphs, mixed content, and multi-error cases must be developed to accurately reflect production readiness.
|
| 229 |
+
""")
|
| 230 |
+
|
| 231 |
+
print(f"Report generated at {OUTPUT_FILE}")
|
archive/old_tests/phase10_helpers/show_samples.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import random
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
|
| 5 |
+
GOLD_DIR = Path('d:/BAYAN2/tests/phase10/gold_datasets')
|
| 6 |
+
OUTPUT_PATH = Path('d:/BAYAN2/reports/benchmark_samples.md')
|
| 7 |
+
|
| 8 |
+
datasets = {
|
| 9 |
+
'Spelling': 'spelling.json',
|
| 10 |
+
'Grammar': 'grammar.json',
|
| 11 |
+
'Punctuation': 'punctuation.json',
|
| 12 |
+
'Entities': 'entities.json',
|
| 13 |
+
'Religious': 'religious.json',
|
| 14 |
+
'Structured': 'structured_content.json',
|
| 15 |
+
'Hallucination': 'hallucination.json'
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
with open(OUTPUT_PATH, 'w', encoding='utf-8') as out:
|
| 19 |
+
out.write("# Benchmark Random Samples (30 per Dataset)\n\n")
|
| 20 |
+
out.write("These are randomly selected samples exactly as stored in the JSON benchmark files.\n\n")
|
| 21 |
+
|
| 22 |
+
random.seed(123) # for reproducibility if run again
|
| 23 |
+
|
| 24 |
+
for name, file in datasets.items():
|
| 25 |
+
out.write(f"## {name}\n\n")
|
| 26 |
+
try:
|
| 27 |
+
with open(GOLD_DIR / file, 'r', encoding='utf-8') as f:
|
| 28 |
+
data = json.load(f)
|
| 29 |
+
|
| 30 |
+
# Select up to 30 samples
|
| 31 |
+
samples = random.sample(data, min(30, len(data)))
|
| 32 |
+
|
| 33 |
+
out.write("```json\n")
|
| 34 |
+
out.write(json.dumps(samples, ensure_ascii=False, indent=2))
|
| 35 |
+
out.write("\n```\n\n")
|
| 36 |
+
except Exception as e:
|
| 37 |
+
out.write(f"Error loading {file}: {e}\n\n")
|
| 38 |
+
|
| 39 |
+
print(f"Generated samples report at {OUTPUT_PATH}")
|
archive/old_tests/phase5_investigation.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Phase 5 — BUG-016/027 Text Duplication Investigation
|
| 3 |
+
|
| 4 |
+
Reproduce exact case: ان الطالبات ذهبو الى الجامعه
|
| 5 |
+
Log every patch produced by spelling and grammar with full ORIGINAL coordinates.
|
| 6 |
+
Determine: overlapping coords (PatchSet bug) vs non-overlapping (coord computation bug).
|
| 7 |
+
Also check: does الى get silently dropped?
|
| 8 |
+
"""
|
| 9 |
+
import sys, os, json, time, requests
|
| 10 |
+
|
| 11 |
+
API_BASE = "https://bayan10-bayan-api.hf.space"
|
| 12 |
+
TIMEOUT = 60
|
| 13 |
+
|
| 14 |
+
def api_call(endpoint, text):
|
| 15 |
+
url = f"{API_BASE}{endpoint}"
|
| 16 |
+
try:
|
| 17 |
+
t0 = time.time()
|
| 18 |
+
resp = requests.post(url, json={"text": text}, timeout=TIMEOUT)
|
| 19 |
+
elapsed = int((time.time() - t0) * 1000)
|
| 20 |
+
if resp.status_code == 200:
|
| 21 |
+
data = resp.json()
|
| 22 |
+
data['_elapsed_ms'] = elapsed
|
| 23 |
+
return data
|
| 24 |
+
return {"error": f"HTTP {resp.status_code}", "_elapsed_ms": elapsed}
|
| 25 |
+
except Exception as e:
|
| 26 |
+
return {"error": f"{type(e).__name__}: {e}"}
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def investigate_bug_016():
|
| 30 |
+
"""Full coordinate investigation for BUG-016."""
|
| 31 |
+
print("=" * 70)
|
| 32 |
+
print("PHASE 5 — BUG-016/027 Text Duplication Investigation")
|
| 33 |
+
print("=" * 70)
|
| 34 |
+
|
| 35 |
+
test_input = "ان الطالبات ذهبو الى الجامعه"
|
| 36 |
+
print(f"\nInput: '{test_input}'")
|
| 37 |
+
print(f"Words: {test_input.split()}")
|
| 38 |
+
for i, w in enumerate(test_input.split()):
|
| 39 |
+
# Compute char offsets
|
| 40 |
+
start = test_input.index(w) if i == 0 else test_input.index(w, sum(len(x) + 1 for x in test_input.split()[:i]))
|
| 41 |
+
end = start + len(w)
|
| 42 |
+
print(f" Word {i}: '{w}' chars [{start}:{end}]")
|
| 43 |
+
|
| 44 |
+
# Track A: Raw model outputs
|
| 45 |
+
print("\n--- Track A: Raw Spelling ---")
|
| 46 |
+
a_spell = api_call("/api/spelling", test_input)
|
| 47 |
+
a_spell_out = a_spell.get("corrected_text", test_input)
|
| 48 |
+
print(f" Input: '{test_input}'")
|
| 49 |
+
print(f" Output: '{a_spell_out}'")
|
| 50 |
+
print(f" Changed: {a_spell_out != test_input}")
|
| 51 |
+
|
| 52 |
+
# Character-level diff
|
| 53 |
+
if a_spell_out != test_input:
|
| 54 |
+
print("\n Character-level changes (spelling):")
|
| 55 |
+
from difflib import SequenceMatcher
|
| 56 |
+
s = SequenceMatcher(None, test_input.split(), a_spell_out.split())
|
| 57 |
+
for tag, i1, i2, j1, j2 in s.get_opcodes():
|
| 58 |
+
if tag != 'equal':
|
| 59 |
+
orig_words = test_input.split()[i1:i2]
|
| 60 |
+
corr_words = a_spell_out.split()[j1:j2]
|
| 61 |
+
print(f" {tag}: [{i1}:{i2}] {orig_words} → [{j1}:{j2}] {corr_words}")
|
| 62 |
+
|
| 63 |
+
print("\n--- Track A: Raw Grammar ---")
|
| 64 |
+
a_gram = api_call("/api/grammar", test_input)
|
| 65 |
+
a_gram_out = a_gram.get("corrected_text", test_input)
|
| 66 |
+
print(f" Input: '{test_input}'")
|
| 67 |
+
print(f" Output: '{a_gram_out}'")
|
| 68 |
+
print(f" Changed: {a_gram_out != test_input}")
|
| 69 |
+
|
| 70 |
+
if a_gram_out != test_input:
|
| 71 |
+
print("\n Character-level changes (grammar):")
|
| 72 |
+
from difflib import SequenceMatcher
|
| 73 |
+
s = SequenceMatcher(None, test_input.split(), a_gram_out.split())
|
| 74 |
+
for tag, i1, i2, j1, j2 in s.get_opcodes():
|
| 75 |
+
if tag != 'equal':
|
| 76 |
+
orig_words = test_input.split()[i1:i2]
|
| 77 |
+
corr_words = a_gram_out.split()[j1:j2]
|
| 78 |
+
print(f" {tag}: [{i1}:{i2}] {orig_words} → [{j1}:{j2}] {corr_words}")
|
| 79 |
+
|
| 80 |
+
# Track B: Full pipeline
|
| 81 |
+
print("\n--- Track B: Full Pipeline ---")
|
| 82 |
+
b = api_call("/api/analyze", test_input)
|
| 83 |
+
b_corrected = b.get("corrected", test_input)
|
| 84 |
+
b_suggestions = b.get("suggestions", [])
|
| 85 |
+
print(f" Input: '{test_input}'")
|
| 86 |
+
print(f" Corrected: '{b_corrected}'")
|
| 87 |
+
print(f" Suggestions: {len(b_suggestions)}")
|
| 88 |
+
|
| 89 |
+
for s in b_suggestions:
|
| 90 |
+
print(f"\n Suggestion [{s.get('start')}:{s.get('end')}]:")
|
| 91 |
+
print(f" Type: {s.get('type')}")
|
| 92 |
+
print(f" Original: '{s.get('original', '')}'")
|
| 93 |
+
print(f" Correction: '{s.get('correction', '')}'")
|
| 94 |
+
if 'confidence' in s:
|
| 95 |
+
print(f" Confidence: {s.get('confidence')}")
|
| 96 |
+
|
| 97 |
+
# Check for duplicates
|
| 98 |
+
print("\n--- Duplicate / Drop Analysis ---")
|
| 99 |
+
output_words = b_corrected.split()
|
| 100 |
+
input_words = test_input.split()
|
| 101 |
+
print(f" Input words: {input_words}")
|
| 102 |
+
print(f" Output words: {output_words}")
|
| 103 |
+
|
| 104 |
+
# Check for duplicated words
|
| 105 |
+
for i, w in enumerate(output_words):
|
| 106 |
+
if i > 0 and w == output_words[i-1]:
|
| 107 |
+
print(f" ⚠ DUPLICATE: '{w}' at positions {i-1} and {i}")
|
| 108 |
+
|
| 109 |
+
# Check for dropped words (الى should appear as الى or إلى)
|
| 110 |
+
for w in input_words:
|
| 111 |
+
# Check if word or a known correction of it appears in output
|
| 112 |
+
found = w in b_corrected
|
| 113 |
+
if not found:
|
| 114 |
+
# Check common corrections
|
| 115 |
+
corrections = {
|
| 116 |
+
'ان': ['أن', 'إن', 'ان'],
|
| 117 |
+
'الى': ['إلى', 'الى'],
|
| 118 |
+
'الجامعه': ['الجامعة', 'الجامعه'],
|
| 119 |
+
'ذهبو': ['ذهبوا', 'ذهبن', 'ذهبو'],
|
| 120 |
+
'الطالبات': ['الطالبات'],
|
| 121 |
+
}
|
| 122 |
+
alts = corrections.get(w, [w])
|
| 123 |
+
found = any(a in b_corrected for a in alts)
|
| 124 |
+
if not found:
|
| 125 |
+
print(f" ⚠ DROPPED: '{w}' not found in corrected output!")
|
| 126 |
+
else:
|
| 127 |
+
print(f" ✓ '{w}' present (or corrected variant)")
|
| 128 |
+
|
| 129 |
+
# Overlap analysis between suggestions
|
| 130 |
+
print("\n--- Overlap Analysis ---")
|
| 131 |
+
for i, s1 in enumerate(b_suggestions):
|
| 132 |
+
for j, s2 in enumerate(b_suggestions):
|
| 133 |
+
if j <= i:
|
| 134 |
+
continue
|
| 135 |
+
s1_start, s1_end = s1.get('start', 0), s1.get('end', 0)
|
| 136 |
+
s2_start, s2_end = s2.get('start', 0), s2.get('end', 0)
|
| 137 |
+
if s1_start < s2_end and s2_start < s1_end:
|
| 138 |
+
print(f" ⚠ OVERLAP: suggestion {i} [{s1_start}:{s1_end}] and suggestion {j} [{s2_start}:{s2_end}]")
|
| 139 |
+
print(f" S{i}: '{s1.get('original','')}' → '{s1.get('correction','')}' ({s1.get('type')})")
|
| 140 |
+
print(f" S{j}: '{s2.get('original','')}' → '{s2.get('correction','')}' ({s2.get('type')})")
|
| 141 |
+
if not any(
|
| 142 |
+
s1.get('start', 0) < s2.get('end', 0) and s2.get('start', 0) < s1.get('end', 0)
|
| 143 |
+
for i, s1 in enumerate(b_suggestions) for j, s2 in enumerate(b_suggestions) if j > i
|
| 144 |
+
):
|
| 145 |
+
print(" ✓ No overlapping suggestions found")
|
| 146 |
+
|
| 147 |
+
return {
|
| 148 |
+
"input": test_input,
|
| 149 |
+
"raw_spelling": a_spell_out,
|
| 150 |
+
"raw_grammar": a_gram_out,
|
| 151 |
+
"pipeline_corrected": b_corrected,
|
| 152 |
+
"suggestions": b_suggestions,
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
if __name__ == "__main__":
|
| 157 |
+
result = investigate_bug_016()
|
| 158 |
+
output_path = os.path.join(os.path.dirname(__file__), 'phase5_results.json')
|
| 159 |
+
with open(output_path, 'w', encoding='utf-8') as f:
|
| 160 |
+
json.dump(result, f, ensure_ascii=False, indent=2)
|
| 161 |
+
print(f"\nResults saved to {output_path}")
|
archive/old_tests/phase5_results.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"input": "ان الطالبات ذهبو الى الجامعه",
|
| 3 |
+
"raw_spelling": "ان الط ابت ذهبوا إلى الجامعه",
|
| 4 |
+
"raw_grammar": "إن الطالبات ذهبن إلى الجامعة",
|
| 5 |
+
"pipeline_corrected": "إن الطالبات ذهبن ذهبوا الجامعة.",
|
| 6 |
+
"suggestions": [
|
| 7 |
+
{
|
| 8 |
+
"alternatives": [],
|
| 9 |
+
"confidence": 1.0,
|
| 10 |
+
"correction": "ذهبن",
|
| 11 |
+
"end": 16,
|
| 12 |
+
"id": "f303a4d8-1369-43f7-8ad5-209c03d4af24",
|
| 13 |
+
"locked": true,
|
| 14 |
+
"original": "ذهبو",
|
| 15 |
+
"priority": 3,
|
| 16 |
+
"start": 12,
|
| 17 |
+
"type": "grammar"
|
| 18 |
+
},
|
| 19 |
+
{
|
| 20 |
+
"alternatives": [],
|
| 21 |
+
"confidence": 0.8,
|
| 22 |
+
"correction": "الجامعة.",
|
| 23 |
+
"end": 28,
|
| 24 |
+
"id": "ece1c300-e501-44dc-9ef2-907b47785145",
|
| 25 |
+
"locked": true,
|
| 26 |
+
"original": "الجامعه",
|
| 27 |
+
"priority": 2,
|
| 28 |
+
"start": 21,
|
| 29 |
+
"type": "punctuation"
|
| 30 |
+
},
|
| 31 |
+
{
|
| 32 |
+
"alternatives": [],
|
| 33 |
+
"confidence": 1.0,
|
| 34 |
+
"correction": "إن",
|
| 35 |
+
"end": 2,
|
| 36 |
+
"id": "aa123654-bb3a-46ab-aa3c-7cea6dc4955b",
|
| 37 |
+
"locked": true,
|
| 38 |
+
"original": "ان",
|
| 39 |
+
"priority": 1,
|
| 40 |
+
"start": 0,
|
| 41 |
+
"type": "spelling"
|
| 42 |
+
},
|
| 43 |
+
{
|
| 44 |
+
"alternatives": [
|
| 45 |
+
"ذهبوا",
|
| 46 |
+
"ال",
|
| 47 |
+
"الم",
|
| 48 |
+
"الى"
|
| 49 |
+
],
|
| 50 |
+
"confidence": 0.9,
|
| 51 |
+
"correction": "ذهبوا",
|
| 52 |
+
"end": 20,
|
| 53 |
+
"id": "bf07637f-0432-4311-aab1-77f521718214",
|
| 54 |
+
"locked": true,
|
| 55 |
+
"original": "الى",
|
| 56 |
+
"priority": 1,
|
| 57 |
+
"start": 17,
|
| 58 |
+
"type": "spelling"
|
| 59 |
+
}
|
| 60 |
+
]
|
| 61 |
+
}
|
archive/old_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()
|
archive/old_tests/phase9_results.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
archive/old_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()
|