youssefreda9 commited on
Commit
4ec8363
·
1 Parent(s): 4540c4b

feat: NLP-2 Grammar model integration (Bayan_Arabic_Grammar T5) - New: src/nlp/grammar/grammar_service.py (lazy T5 loading) - Dockerfile: pre-download grammar model (2.14GB safetensors) - Wire /api/analyze Step 2 to grammar service - Update /api/grammar standalone endpoint - Add _grammar_available() health check

Browse files
Dockerfile CHANGED
@@ -45,6 +45,16 @@ AutoModelForMaskedLM.from_pretrained('aubmindlab/bert-base-arabertv02'); \
45
  print('Spelling model + MLM cached!'); \
46
  "
47
 
 
 
 
 
 
 
 
 
 
 
48
  # Copy application code
49
  COPY src/ ./src/
50
  COPY .env* ./
 
45
  print('Spelling model + MLM cached!'); \
46
  "
47
 
48
+ # 3. Grammar model (T5ForConditionalGeneration)
49
+ RUN python -c "\
50
+ from transformers import AutoTokenizer, AutoModelForSeq2SeqLM; \
51
+ print('Downloading Grammar tokenizer...'); \
52
+ AutoTokenizer.from_pretrained('bayan10/Bayan_Arabic_Grammar'); \
53
+ print('Downloading Grammar T5 model...'); \
54
+ AutoModelForSeq2SeqLM.from_pretrained('bayan10/Bayan_Arabic_Grammar'); \
55
+ print('Grammar model cached!'); \
56
+ "
57
+
58
  # Copy application code
59
  COPY src/ ./src/
60
  COPY .env* ./
src/app.py CHANGED
@@ -155,7 +155,7 @@ def health_check():
155
  'summarization': summarization_model is not None,
156
  'spelling': _spelling_available(),
157
  'autocomplete': False,
158
- 'grammar': False,
159
  'punctuation': False
160
  },
161
  'note': 'Free tier: summarization local, other models return input unchanged',
@@ -232,6 +232,15 @@ def _spelling_available():
232
  return False
233
 
234
 
 
 
 
 
 
 
 
 
 
235
  @app.route('/api/spelling', methods=['POST'])
236
  def spelling_correction():
237
  """
@@ -443,12 +452,6 @@ def grammar_correction():
443
  "text": "Arabic text to correct"
444
  }
445
  """
446
- if not USE_HF_API and grammar_model is None:
447
- return jsonify({
448
- 'error': 'Grammar model not loaded. Please check server logs.',
449
- 'status': 'error'
450
- }), 503
451
-
452
  try:
453
  if not request.is_json:
454
  return jsonify({'error': 'Request must be JSON', 'status': 'error'}), 400
@@ -460,17 +463,13 @@ def grammar_correction():
460
  return jsonify({'error': 'Text is required', 'status': 'error'}), 400
461
 
462
  logger.info(f"Correcting grammar for text of length: {len(text)}")
463
- if USE_HF_API:
464
- # Grammar uses spelling model as proxy (no dedicated grammar model yet)
465
- corrected = hf_correct_spelling(text)
466
- else:
467
- corrected = grammar_model.correct(text)
468
 
469
  return jsonify({
470
- 'corrected': corrected,
471
- 'status': 'success',
472
- 'original_length': len(text),
473
- 'corrected_length': len(corrected)
474
  })
475
 
476
  except Exception as e:
@@ -850,15 +849,13 @@ def analyze_text():
850
  logger.error(f"[ANALYZE] Spelling failed: {e}")
851
 
852
  # 2. Grammar (runs on spelling-corrected text)
853
- has_grammar = USE_HF_API or grammar_model
854
  if has_grammar:
855
  try:
856
  t0 = time.time()
857
  logger.info(f"[ANALYZE] Step 2: Grammar correction starting...")
858
- if USE_HF_API:
859
- corrected_grammar = hf_correct_spelling(current_text)
860
- else:
861
- corrected_grammar = grammar_model.correct(current_text)
862
  logger.info(f"[ANALYZE] Step 2: Grammar done in {time.time()-t0:.2f}s")
863
  if corrected_grammar != current_text:
864
  diffs = get_word_diffs(current_text, corrected_grammar)
 
155
  'summarization': summarization_model is not None,
156
  'spelling': _spelling_available(),
157
  'autocomplete': False,
158
+ 'grammar': _grammar_available(),
159
  'punctuation': False
160
  },
161
  'note': 'Free tier: summarization local, other models return input unchanged',
 
232
  return False
233
 
234
 
235
+ def _grammar_available():
236
+ """Check if grammar model is loaded (without triggering lazy load)."""
237
+ try:
238
+ from nlp.grammar.grammar_service import _grammar_model
239
+ return _grammar_model is not None
240
+ except Exception:
241
+ return False
242
+
243
+
244
  @app.route('/api/spelling', methods=['POST'])
245
  def spelling_correction():
246
  """
 
452
  "text": "Arabic text to correct"
453
  }
454
  """
 
 
 
 
 
 
455
  try:
456
  if not request.is_json:
457
  return jsonify({'error': 'Request must be JSON', 'status': 'error'}), 400
 
463
  return jsonify({'error': 'Text is required', 'status': 'error'}), 400
464
 
465
  logger.info(f"Correcting grammar for text of length: {len(text)}")
466
+ from nlp.grammar.grammar_service import correct_grammar
467
+ corrected = correct_grammar(text)
 
 
 
468
 
469
  return jsonify({
470
+ 'original_text': text,
471
+ 'corrected_text': corrected,
472
+ 'status': 'success'
 
473
  })
474
 
475
  except Exception as e:
 
849
  logger.error(f"[ANALYZE] Spelling failed: {e}")
850
 
851
  # 2. Grammar (runs on spelling-corrected text)
852
+ has_grammar = True # Always available via lazy-loaded grammar_service
853
  if has_grammar:
854
  try:
855
  t0 = time.time()
856
  logger.info(f"[ANALYZE] Step 2: Grammar correction starting...")
857
+ from nlp.grammar.grammar_service import correct_grammar
858
+ corrected_grammar = correct_grammar(current_text)
 
 
859
  logger.info(f"[ANALYZE] Step 2: Grammar done in {time.time()-t0:.2f}s")
860
  if corrected_grammar != current_text:
861
  diffs = get_word_diffs(current_text, corrected_grammar)
src/nlp/grammar/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Grammar NLP package
src/nlp/grammar/grammar_service.py ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Grammar correction service using Bayan_Arabic_Grammar (T5).
3
+ Lazy-loads the model on first request, keeps it resident in memory.
4
+ """
5
+ import logging
6
+ import torch
7
+ from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ _grammar_model = None
12
+ _grammar_tokenizer = None
13
+ _device = None
14
+
15
+ MODEL_ID = "bayan10/Bayan_Arabic_Grammar"
16
+
17
+ # Generation parameters
18
+ MAX_INPUT_LENGTH = 512
19
+ MAX_OUTPUT_LENGTH = 512
20
+ NUM_BEAMS = 4
21
+
22
+
23
+ def get_grammar_model():
24
+ """
25
+ Lazy-load the grammar model. Returns (model, tokenizer, device).
26
+ Loads once, stays in memory forever.
27
+ """
28
+ global _grammar_model, _grammar_tokenizer, _device
29
+
30
+ if _grammar_model is not None:
31
+ return _grammar_model, _grammar_tokenizer, _device
32
+
33
+ logger.info("Loading Grammar model (lazy init)...")
34
+
35
+ _device = "cpu"
36
+
37
+ try:
38
+ logger.info(f"Loading tokenizer: {MODEL_ID}")
39
+ _grammar_tokenizer = AutoTokenizer.from_pretrained(
40
+ MODEL_ID, local_files_only=True
41
+ )
42
+
43
+ logger.info(f"Loading T5 model: {MODEL_ID}")
44
+ _grammar_model = AutoModelForSeq2SeqLM.from_pretrained(
45
+ MODEL_ID, local_files_only=True
46
+ )
47
+ _grammar_model.to(_device)
48
+ _grammar_model.eval()
49
+
50
+ logger.info(f"Grammar model loaded on {_device}")
51
+
52
+ except Exception as e:
53
+ logger.error(f"Failed to load grammar model: {e}")
54
+ # Try without local_files_only as fallback
55
+ try:
56
+ logger.info("Retrying without local_files_only...")
57
+ _grammar_tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
58
+ _grammar_model = AutoModelForSeq2SeqLM.from_pretrained(MODEL_ID)
59
+ _grammar_model.to(_device)
60
+ _grammar_model.eval()
61
+ logger.info(f"Grammar model loaded on {_device} (fallback)")
62
+ except Exception as e2:
63
+ logger.error(f"Grammar model load failed completely: {e2}")
64
+ _grammar_model = None
65
+ _grammar_tokenizer = None
66
+ raise
67
+
68
+ return _grammar_model, _grammar_tokenizer, _device
69
+
70
+
71
+ def correct_grammar(text: str) -> str:
72
+ """
73
+ Run grammar correction on input text using T5.
74
+ Returns the corrected text.
75
+ """
76
+ if not text or not text.strip():
77
+ return text
78
+
79
+ model, tokenizer, device = get_grammar_model()
80
+
81
+ try:
82
+ # Tokenize input
83
+ inputs = tokenizer(
84
+ text,
85
+ return_tensors="pt",
86
+ max_length=MAX_INPUT_LENGTH,
87
+ truncation=True,
88
+ padding=True
89
+ )
90
+ inputs = {k: v.to(device) for k, v in inputs.items()}
91
+
92
+ # Generate correction
93
+ with torch.no_grad():
94
+ outputs = model.generate(
95
+ **inputs,
96
+ max_length=MAX_OUTPUT_LENGTH,
97
+ num_beams=NUM_BEAMS,
98
+ early_stopping=True,
99
+ no_repeat_ngram_size=3,
100
+ )
101
+
102
+ # Decode output
103
+ corrected = tokenizer.decode(outputs[0], skip_special_tokens=True)
104
+
105
+ # Safety: if model returns empty or much shorter, keep original
106
+ if not corrected or not corrected.strip():
107
+ return text
108
+
109
+ # If the correction is drastically different (>80% changed), keep original
110
+ if len(corrected) < len(text) * 0.3:
111
+ logger.warning(f"Grammar output too short ({len(corrected)} vs {len(text)}), keeping original")
112
+ return text
113
+
114
+ return corrected.strip()
115
+
116
+ except Exception as e:
117
+ logger.error(f"Grammar correction failed: {e}")
118
+ return text