Skip to content

Commit b3ba499

Browse files
committed
html table debugged
1 parent fa313c8 commit b3ba499

7 files changed

Lines changed: 117 additions & 7 deletions

File tree

docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ services:
55
ocr-model:
66
build: ./ocr-model
77
runtime: nvidia
8+
ipc: host
89
restart: unless-stopped
910
environment:
1011
- NVIDIA_VISIBLE_DEVICES=all

ocr-model/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM vllm/vllm-openai:latest
1+
FROM vllm/vllm-openai:cu129-nightly
22
ENTRYPOINT ["python3", "-m", "vllm.entrypoints.openai.api_server"]
33
CMD [ \
44
"--model", "deepseek-ai/DeepSeek-OCR-2", \

ocr_pipeline/services/startup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ async def wait_for_model_server() -> bool:
5353
)
5454
await asyncio.sleep(interval)
5555
continue
56-
except (httpx.ConnectError, httpx.ReadTimeout, httpx.ConnectTimeout) as exc:
56+
except httpx.HTTPError as exc:
5757
model_readiness.error = f"connection failed ({type(exc).__name__})"
5858
logger.info(
5959
"Model server not reachable yet (%s)", model_readiness.error

ocr_pipeline/services/text_cleaner.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import html
12
import re
23
import unicodedata
34

@@ -43,6 +44,15 @@ class TextCleaner:
4344

4445
# End-of-line soft hyphens: "word-\n" or "word- \n" followed by continuation
4546
_HYPHEN_RE = re.compile(r"(\w)- ?\n(\w)")
47+
_TABLE_RE = re.compile(r"<table\b[^>]*>.*?</table>", re.IGNORECASE | re.DOTALL)
48+
_ROW_RE = re.compile(r"<tr\b[^>]*>(.*?)</tr>", re.IGNORECASE | re.DOTALL)
49+
_CELL_RE = re.compile(r"<t[dh]\b[^>]*>(.*?)</t[dh]>", re.IGNORECASE | re.DOTALL)
50+
_BR_RE = re.compile(r"<br\s*/?>", re.IGNORECASE)
51+
_PARA_END_RE = re.compile(r"</(?:p|div|h[1-6])\s*>", re.IGNORECASE)
52+
_HTML_TAG_RE = re.compile(
53+
r"</?(?:center|div|span|html|body|table|thead|tbody|tfoot|tr|td|th|p|br|h[1-6])\b[^>]*>",
54+
re.IGNORECASE,
55+
)
4656

4757
def clean(self, text: str, strip_refs: bool = False) -> str:
4858
"""Full cleaning pipeline."""
@@ -53,6 +63,7 @@ def clean(self, text: str, strip_refs: bool = False) -> str:
5363
text = self._strip_ref_blocks(text)
5464
text = self._strip_model_tokens(text)
5565
text = self._strip_artifacts(text)
66+
text = self._html_to_text(text)
5667
text = self._rejoin_hyphens(text)
5768
text = self._normalize_whitespace(text)
5869
text = self._fix_common_ocr_issues(text)
@@ -96,6 +107,30 @@ def _strip_artifacts(self, text: str) -> str:
96107
text = pattern.sub("", text)
97108
return text
98109

110+
def _html_to_text(self, text: str) -> str:
111+
"""Convert occasional model-emitted HTML into readable plain text."""
112+
text = self._TABLE_RE.sub(lambda match: self._table_to_text(match.group(0)), text)
113+
text = self._BR_RE.sub("\n", text)
114+
text = self._PARA_END_RE.sub("\n", text)
115+
text = self._HTML_TAG_RE.sub("", text)
116+
return html.unescape(text)
117+
118+
def _table_to_text(self, table: str) -> str:
119+
rows: list[str] = []
120+
121+
for row_match in self._ROW_RE.finditer(table):
122+
cells: list[str] = []
123+
for cell_match in self._CELL_RE.finditer(row_match.group(1)):
124+
cell = self._HTML_TAG_RE.sub("", cell_match.group(1))
125+
cell = html.unescape(cell)
126+
cell = re.sub(r"\s+", " ", cell).strip()
127+
if cell:
128+
cells.append(cell)
129+
if cells:
130+
rows.append(" | ".join(cells))
131+
132+
return "\n".join(rows)
133+
99134
def _normalize_whitespace(self, text: str) -> str:
100135
# Replace multiple blank lines with a single blank line
101136
text = re.sub(r"\n{3,}", "\n\n", text)

ocr_pipeline/static/js/app.js

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ function opencrApp() {
120120
toasts: [],
121121

122122
_stream: new RunStream(),
123+
_runPollTimer: null,
123124

124125
async init() {
125126
await Promise.all([
@@ -194,12 +195,18 @@ function opencrApp() {
194195

195196
async refreshSelectedRun() {
196197
if (!this.selectedRunId) return;
197-
try { this.selectedRun = await API.getRun(this.selectedRunId); }
198+
try {
199+
this.selectedRun = await API.getRun(this.selectedRunId);
200+
if (!['queued', 'processing'].includes(this.selectedRun.status)) {
201+
this.stopRunPolling();
202+
}
203+
}
198204
catch (e) { this.toast(`Failed to refresh run: ${e.message}`, 'error'); }
199205
},
200206

201207
async selectRun(runId) {
202208
this._stream.disconnect();
209+
this.stopRunPolling();
203210
if (!runId) {
204211
this.activeView = 'documents';
205212
this.selectedRunId = null;
@@ -216,17 +223,18 @@ function opencrApp() {
216223
const firstCompleted = (this.selectedRun.documents || []).find(d => d.status === 'completed');
217224
if (firstCompleted) await this.openDocument(firstCompleted.document_id);
218225
else this.inspector = emptyInspector();
219-
if (['queued', 'processing'].includes(this.selectedRun.status)) this.connectStream(runId);
226+
if (['queued', 'processing'].includes(this.selectedRun.status)) {
227+
this.connectStream(runId);
228+
this.startRunPolling();
229+
}
220230
} catch (e) {
221231
this.toast(`Failed to load run: ${e.message}`, 'error');
222232
}
223233
},
224234

225235
connectStream(runId) {
226236
this._stream.connect(runId, async (event) => {
227-
if (event.type === 'page_complete' || event.type === 'document_complete') {
228-
await this.refreshSelectedRun();
229-
}
237+
await this.refreshSelectedRun();
230238
if (event.type === 'run_complete' || event.type === 'run_failed') {
231239
await Promise.all([this.refreshSelectedRun(), this.refreshRuns(), this.refreshMetrics()]);
232240
const completed = event.type === 'run_complete';
@@ -236,6 +244,21 @@ function opencrApp() {
236244
});
237245
},
238246

247+
startRunPolling() {
248+
this.stopRunPolling();
249+
this._runPollTimer = setInterval(async () => {
250+
if (!this.selectedRunId) return this.stopRunPolling();
251+
await Promise.all([this.refreshSelectedRun(), this.refreshRuns(), this.refreshMetrics()]);
252+
}, 2000);
253+
},
254+
255+
stopRunPolling() {
256+
if (this._runPollTimer) {
257+
clearInterval(this._runPollTimer);
258+
this._runPollTimer = null;
259+
}
260+
},
261+
239262
async openDocument(documentId) {
240263
if (!this.selectedRunId || !documentId) return;
241264
this.inspector.documentId = documentId;

tests/test_gpu_first_runtime.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1+
import asyncio
12
from pathlib import Path
23
from typing import get_args
34

5+
import httpx
6+
47
from ocr_pipeline.config import Settings, settings
58
from ocr_pipeline.models.schemas import HealthResponse
9+
from ocr_pipeline.services import startup
610
from ocr_pipeline.services.startup import ModelReadiness
711

812

@@ -31,3 +35,26 @@ def test_local_backend_dependency_file_is_removed():
3135
repo_root = Path(__file__).parents[1]
3236

3337
assert not (repo_root / "requirements-local.txt").exists()
38+
39+
40+
def test_model_readiness_treats_read_error_as_waiting(monkeypatch):
41+
class DroppingAsyncClient:
42+
def __init__(self, *args, **kwargs):
43+
pass
44+
45+
async def __aenter__(self):
46+
return self
47+
48+
async def __aexit__(self, *args):
49+
return None
50+
51+
async def get(self, url):
52+
raise httpx.ReadError("connection dropped")
53+
54+
monkeypatch.setattr(startup.httpx, "AsyncClient", DroppingAsyncClient)
55+
monkeypatch.setattr(startup.settings, "model_ready_timeout", 0.001)
56+
monkeypatch.setattr(startup.settings, "model_ready_interval", 0.001)
57+
monkeypatch.setattr(startup, "model_readiness", ModelReadiness())
58+
59+
assert asyncio.run(startup.wait_for_model_server()) is False
60+
assert startup.model_readiness.error == "connection failed (ReadError)"

tests/test_text_cleaner.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,30 @@ def test_strips_null_bytes(self, cleaner):
7474
assert "HelloWorld" in result
7575

7676

77+
class TestHtmlCleanup:
78+
def test_converts_html_table_to_plain_text_rows(self, cleaner):
79+
text = (
80+
"<table><tr><td>Köre almagan yiğittin,</td><td>Göremeyen yiğidin,</td></tr>"
81+
"<tr><td>Kökiregi tüyilsin.</td><td>Göğsü duralsın.</td></tr></table>"
82+
)
83+
84+
result = cleaner.clean(text)
85+
86+
assert result == (
87+
"Köre almagan yiğittin, | Göremeyen yiğidin,\n"
88+
"Kökiregi tüyilsin. | Göğsü duralsın."
89+
)
90+
assert "<table" not in result
91+
assert "<td" not in result
92+
93+
def test_unescapes_html_entities(self, cleaner):
94+
text = "&quot;Yüzü de ak dana, Şarifulla&#x27;nın giydiği"
95+
96+
result = cleaner.clean(text)
97+
98+
assert result == '"Yüzü de ak dana, Şarifulla\'nın giydiği'
99+
100+
77101
class TestWhitespaceNormalization:
78102
def test_multiple_blank_lines(self, cleaner):
79103
text = "Line 1\n\n\n\n\nLine 2"

0 commit comments

Comments
 (0)