|
| 1 | +name: Apply verified OCR separator fix |
| 2 | + |
| 3 | +on: |
| 4 | + push: |
| 5 | + branches: [security-fixes-clean] |
| 6 | + paths: |
| 7 | + - '.github/workflows/apply-ocr-fix.yml' |
| 8 | + |
| 9 | +permissions: |
| 10 | + contents: write |
| 11 | + |
| 12 | +jobs: |
| 13 | + apply: |
| 14 | + runs-on: ubuntu-latest |
| 15 | + steps: |
| 16 | + - uses: actions/checkout@v4 |
| 17 | + |
| 18 | + - name: Patch production OCR implementation |
| 19 | + run: | |
| 20 | + python - <<'PY' |
| 21 | + from pathlib import Path |
| 22 | + p = Path('hf-space/app_core.py') |
| 23 | + s = p.read_text(encoding='utf-8') |
| 24 | + old_block = '''OCR_CORRECTIONS = { |
| 25 | + "باراسيتبمول": "باراسيتامول", "ايبوروفين": "ايبوبروفين", |
| 26 | + "اموكسيستلين": "اموكسيسيلين", "اموكسيسلين": "اموكسيسيلين", |
| 27 | + "ازيثروميسين": "ازيثرومايسين", "ميتروندازول": "ميترونيدازول", |
| 28 | + "ديكلوفيناك ": "ديكلوفيناك", "اوجمينتين": "اوجمنتين", |
| 29 | + "اوميبرازول ": "اوميبرازول", "سيليبريكس ": "سيليبريكس", |
| 30 | + "ترامادول ": "ترامادول", "كاتافلام ": "كاتافلام", |
| 31 | + "نوفافين ": "نوفافين", "فلاميكس ": "فلاميكس", |
| 32 | + "بنادول ": "بنادول", "ادفيل ": "ادفيل", |
| 33 | + }''' |
| 34 | + new_block = '''OCR_CORRECTIONS = { |
| 35 | + "باراسيتبمول": "باراسيتامول", "ايبوروفين": "ايبوبروفين", |
| 36 | + "اموكسيستلين": "اموكسيسيلين", "اموكسيسلين": "اموكسيسيلين", |
| 37 | + "ازيثروميسين": "ازيثرومايسين", "ميتروندازول": "ميترونيدازول", |
| 38 | + "ديكلوفيناك": "ديكلوفيناك", "اوجمينتين": "اوجمنتين", |
| 39 | + "اوميبرازول": "اوميبرازول", "سيليبريكس": "سيليبريكس", |
| 40 | + "ترامادول": "ترامادول", "كاتافلام": "كاتافلام", |
| 41 | + "نوفافين": "نوفافين", "فلاميكس": "فلاميكس", |
| 42 | + "بنادول": "بنادول", "ادفيل": "ادفيل", |
| 43 | + }''' |
| 44 | + if old_block not in s: |
| 45 | + raise SystemExit('Expected OCR_CORRECTIONS block not found; refusing blind edit') |
| 46 | + s = s.replace(old_block, new_block, 1) |
| 47 | + old_fn = '''def _auto_correct_ocr(text: str) -> Tuple[str, List[Dict]]: |
| 48 | + """Apply OCR corrections + spell checker. Returns (corrected, changes).""" |
| 49 | + changes = [] |
| 50 | + corrected = text |
| 51 | + for wrong, right in OCR_CORRECTIONS.items(): |
| 52 | + if wrong in corrected: |
| 53 | + count = corrected.count(wrong) |
| 54 | + corrected = corrected.replace(wrong, right) |
| 55 | + changes.append({"type": "ocr_fix", "from": wrong, "to": right, "count": count}) |
| 56 | + corrected = re.sub(r'[ \\t]+', ' ', corrected) |
| 57 | + corrected = re.sub(r'\\n{3,}', '\\n\\n', corrected).strip() |
| 58 | + return corrected, changes''' |
| 59 | + new_fn = '''def _auto_correct_ocr(text: str) -> Tuple[str, List[Dict]]: |
| 60 | + """Apply OCR corrections while preserving token boundaries and separators.""" |
| 61 | + changes = [] |
| 62 | + corrected = text |
| 63 | + for wrong, right in OCR_CORRECTIONS.items(): |
| 64 | + if wrong == right: |
| 65 | + continue |
| 66 | + pattern = r'(?<!\\w)' + re.escape(wrong) + r'(?!\\w)' |
| 67 | + matches = list(re.finditer(pattern, corrected)) |
| 68 | + if matches: |
| 69 | + corrected = re.sub(pattern, right, corrected) |
| 70 | + changes.append({"type": "ocr_fix", "from": wrong, "to": right, "count": len(matches)}) |
| 71 | + corrected = re.sub(r'[ \\t]+', ' ', corrected) |
| 72 | + corrected = re.sub(r'\\n{3,}', '\\n\\n', corrected).strip() |
| 73 | + return corrected, changes''' |
| 74 | + if old_fn not in s: |
| 75 | + raise SystemExit('Expected _auto_correct_ocr body not found; refusing blind edit') |
| 76 | + s = s.replace(old_fn, new_fn, 1) |
| 77 | + p.write_text(s, encoding='utf-8') |
| 78 | + PY |
| 79 | +
|
| 80 | + - name: Add behavioral regression test |
| 81 | + run: | |
| 82 | + mkdir -p tests/security |
| 83 | + cat > tests/security/test_medical_behavior.py <<'PY' |
| 84 | + import re |
| 85 | + from pathlib import Path |
| 86 | + |
| 87 | + SOURCE = Path('hf-space/app_core.py').read_text(encoding='utf-8') |
| 88 | + |
| 89 | + def _load_auto_correct(): |
| 90 | + start = SOURCE.index('OCR_CORRECTIONS = {') |
| 91 | + end = SOURCE.index('\n\n# ── Lazy-loaded modules', start) |
| 92 | + namespace = {} |
| 93 | + exec(SOURCE[start:end], namespace) |
| 94 | + fn_start = SOURCE.index('def _auto_correct_ocr(') |
| 95 | + fn_end = SOURCE.index('\n\ndef _extract_ner', fn_start) |
| 96 | + exec('import re\nfrom typing import Dict, List, Tuple\n' + SOURCE[fn_start:fn_end], namespace) |
| 97 | + return namespace['_auto_correct_ocr'] |
| 98 | + |
| 99 | + def test_medical_ocr_boundaries_and_tokens(): |
| 100 | + fn = _load_auto_correct() |
| 101 | + cases = [ |
| 102 | + ('ترامادول 0.5 mg', 'ترامادول 0.5 mg'), |
| 103 | + ('0.5', '0.5'), ('1.25', '1.25'), ('0.75', '0.75'), |
| 104 | + ('٠٫٥', '٠٫٥'), ('١٫٢٥', '١٫٢٥'), |
| 105 | + ('لا', 'لا'), ('ليس', 'ليس'), ('لم', 'لم'), ('لن', 'لن'), |
| 106 | + ('لا يوجد سكري', 'لا يوجد سكري'), |
| 107 | + ('لا يعطى ترامادول 0.5 mg', 'لا يعطى ترامادول 0.5 mg'), |
| 108 | + ('باراسيتبمول 500 mg', 'باراسيتامول 500 mg'), |
| 109 | + ] |
| 110 | + for src, expected in cases: |
| 111 | + actual, _ = fn(src) |
| 112 | + assert actual == expected, f'{src!r} -> {actual!r}, expected {expected!r}' |
| 113 | + |
| 114 | + def test_no_trailing_space_correction_keys(): |
| 115 | + namespace = {} |
| 116 | + start = SOURCE.index('OCR_CORRECTIONS = {') |
| 117 | + end = SOURCE.index('\n\n# ── Lazy-loaded modules', start) |
| 118 | + exec(SOURCE[start:end], namespace) |
| 119 | + assert all(not key.endswith(' ') for key in namespace['OCR_CORRECTIONS']) |
| 120 | + PY |
| 121 | +
|
| 122 | + - name: Run focused behavioral tests |
| 123 | + run: python -m pytest tests/security/test_medical_behavior.py -q |
| 124 | + |
| 125 | + - name: Verify admin and deployment changes |
| 126 | + run: | |
| 127 | + grep -q 'Depends(get_current_user)' app/routers/admin.py |
| 128 | + test -f deploy/docker-compose.full.yml |
| 129 | + test ! -f deploy/docker-compose.prod.yml |
| 130 | + grep -q 'docker-compose.full.yml' deploy/deploy.sh |
| 131 | + ! grep -Eq '(^|[[:space:]])cp[[:space:]].*docker-compose.*docker-compose.prod.yml' deploy/deploy.sh |
| 132 | +
|
| 133 | + - name: Commit verified changes |
| 134 | + run: | |
| 135 | + git config user.name 'github-actions[bot]' |
| 136 | + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' |
| 137 | + git add hf-space/app_core.py tests/security/test_medical_behavior.py |
| 138 | + git commit -m 'fix: preserve medical OCR token boundaries' || exit 0 |
| 139 | + git push origin HEAD:security-fixes-clean |
0 commit comments