Skip to content

Commit 1bd7631

Browse files
committed
ci: apply verified OCR separator fix
1 parent f00bf6e commit 1bd7631

1 file changed

Lines changed: 139 additions & 0 deletions

File tree

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
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

Comments
 (0)