Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 6 additions & 18 deletions .github/workflows/github-actions-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -117,18 +117,8 @@ jobs:
include:
- runs-on: macos-latest
install-poppler: brew install poppler
pytest-args: >-
--ignore=tests/cv_models
tests/
- runs-on: ubuntu-24.04-arm
install-poppler: sudo apt-get update && sudo apt-get install -y poppler-utils
pytest-args: >-
tests/external/pdfalto/url_test.py
tests/external/pdfalto/parser_test.py
tests/document
tests/utils
tests/config
tests/lookup
runs-on: ${{ matrix.runs-on }}
steps:
- name: Check out repository code
Expand All @@ -144,16 +134,14 @@ jobs:

- name: Install dependencies
run: uv sync --frozen --group dev --extra delft
if: runner.os != 'Linux' || runner.arch != 'ARM64'

- name: Install dependencies (Linux arm64 - skip delft extras with no arm64 wheels)
run: |
uv sync --frozen --group dev
uv pip install "sciencebeam-trainer-delft>=0.0.36"
if: runner.os == 'Linux' && runner.arch == 'ARM64'

# the slow cases download model artifacts; the Docker pytest target runs them
- name: Run pytest
run: uv run python -m pytest -p no:cacheprovider ${{ matrix.pytest-args }}
run: >-
uv run python -m pytest -p no:cacheprovider
-m "not slow"
--ignore=tests/cv_models
tests/


testpypi-publish:
Expand Down
4 changes: 4 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ COPY docker/entrypoint.sh ./docker/entrypoint.sh

ENV SCIENCEBEAM_DELFT_MAX_SEQUENCE_LENGTH=2000
ENV SCIENCEBEAM_DELFT_INPUT_WINDOW_STRIDE=1800
# torch would otherwise pick CUDA whenever the host happens to expose a GPU
ENV SCIENCEBEAM_DELFT_DEVICE=cpu

CMD [ "--port=8070", "--host=0.0.0.0" ]
ENTRYPOINT ["/usr/bin/dumb-init", "--", "/opt/sciencebeam_parser/docker/entrypoint.sh"]
Expand All @@ -197,6 +199,8 @@ COPY docker/entrypoint.sh ./docker/entrypoint.sh

ENV SCIENCEBEAM_DELFT_MAX_SEQUENCE_LENGTH=2000
ENV SCIENCEBEAM_DELFT_INPUT_WINDOW_STRIDE=1800
# torch would otherwise pick CUDA whenever the host happens to expose a GPU
ENV SCIENCEBEAM_DELFT_DEVICE=cpu

# temporary workaround for tesserocr https://github.com/sirfz/tesserocr/issues/165
ENV LC_ALL=C
Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,8 @@ dev-venv: venv-create dev-install

# Lightweight host venv for running the benchmark scripts (incl. benchmarks.run)
# directly on the host. Installs only the benchmark group + light core deps, NOT the
# cpu/delft/cv extras (torch/TensorFlow), so it works where dev-install can't (e.g. Mac
# Intel). benchmarks.run orchestrates the GROBID/parser containers via the host docker CLI.
# cpu/delft/cv extras (torch), so it works where dev-install can't (e.g. Mac Intel).
# benchmarks.run orchestrates the GROBID/parser containers via the host docker CLI.
benchmark-install:
$(UV) sync --active --frozen --no-dev --group benchmark

Expand Down
38 changes: 38 additions & 0 deletions doc/python_library.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,44 @@ ScienceBeam Parser allows you to parse scientific documents. It provides a REST
pip install sciencebeam-parser[delft,cpu]
```

The `delft` extra provides the PyTorch-based sequence labelling engine. There is no TensorFlow
extra: the delft engine runs on PyTorch, and TF-era model artifacts are converted to a torch
state dict in memory when they are loaded, so the model URLs in the
[default config.yml](../sciencebeam_parser/resources/default_config/config.yml) need no change
and the artifacts themselves are never modified.

### Installing CPU-only PyTorch

On Linux the default PyTorch wheel on PyPI is the CUDA build, which adds several `nvidia-*`
packages and `triton` that a CPU-only deployment never uses. Index configuration is not part of
published package metadata, so this project's own cannot reach you — install torch from the CPU
index yourself, before the rest:

```bash
pip install torch --index-url https://download.pytorch.org/whl/cpu
pip install sciencebeam-parser[delft,cpu]
```

With `uv`, declare `torch` as a direct dependency of your own project and point it at the CPU
index. Declaring it directly is what makes the source apply — receiving torch only through
`sciencebeam-parser` leaves it resolving from PyPI:

```toml
[project]
dependencies = [
"sciencebeam-parser[delft,cpu]",
"torch",
]

[tool.uv.sources]
torch = [{ index = "torch-cpu" }]

[[tool.uv.index]]
name = "torch-cpu"
url = "https://download.pytorch.org/whl/cpu"
explicit = true
```

## CLI

### CLI: Start Server
Expand Down
11 changes: 5 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ dependencies = [
]

[project.optional-dependencies]
# torch is declared here, rather than left to arrive through delft, so that
# [tool.uv.sources] binds it to the CPU index across the whole lock
cpu = [
"torch>=2.5.1",
"torchvision>=0.20.1"
"torch>=2.11.0",
"torchvision>=0.26.0"
]
delft = [
"sciencebeam-trainer-delft[delft]>=0.0.38",
"sciencebeam-trainer-delft[delft]>=1.0.1",
]
cv = [
"layoutparser==0.3.2",
Expand All @@ -46,9 +48,6 @@ dev = [
ocr = [
"tesserocr==2.5.2",
]
tf = [
"tensorflow>=2.17.1",
]


[tool.uv.sources]
Expand Down
84 changes: 84 additions & 0 deletions tests/models/delft_model_impl_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import logging

import pytest
from sciencebeam_trainer_delft.sequence_labelling.reader import load_data_crf_lines
from sciencebeam_trainer_delft.utils.download_manager import DownloadManager

from sciencebeam_parser.app.context import AppContext
from sciencebeam_parser.config.config import AppConfig, get_download_dir
from sciencebeam_parser.document.layout_document import (
LayoutBlock,
LayoutDocument,
LayoutLine,
LayoutToken
)
from sciencebeam_parser.external.wapiti.wrapper import LazyWapitiBinaryWrapper
from sciencebeam_parser.models.data import DEFAULT_DOCUMENT_FEATURES_CONTEXT
from sciencebeam_parser.models.delft_model_impl import DelftModelImpl
from sciencebeam_parser.models.header.data import HeaderDataGenerator
from sciencebeam_parser.models.model import iter_data_lines_for_model_data_iterables
from sciencebeam_parser.resources.default_config import DEFAULT_CONFIG_FILE


LOGGER = logging.getLogger(__name__)


# The biorxiv_elife header model: TF-era `model_weights.hdf5`, and no word embeddings,
# so loading it needs nothing from the embedding registry.
HEADER_MODEL_URL = (
'https://github.com/eLifePathways/sciencebeam-models/releases/download'
'/v0.0.1/2020-10-04-delft-grobid-header-biorxiv-no-word-embedding.tar.gz'
)

HEADER_LINE_TOKEN_TEXTS = [
['A', 'Study', 'of', 'Something'],
['Jane', 'Doe', 'and', 'John', 'Smith']
]


@pytest.fixture(name='app_context', scope='module')
def _app_context() -> AppContext:
app_config = AppConfig.load_yaml(DEFAULT_CONFIG_FILE)
download_manager = DownloadManager(download_dir=get_download_dir(app_config))
return AppContext(
app_config=app_config,
download_manager=download_manager,
lazy_wapiti_binary_wrapper=LazyWapitiBinaryWrapper(
download_manager=download_manager
)
)


def _get_layout_document() -> LayoutDocument:
return LayoutDocument.for_blocks([
LayoutBlock(lines=[
LayoutLine([LayoutToken(text) for text in line_token_texts])
for line_token_texts in HEADER_LINE_TOKEN_TEXTS
])
])


@pytest.mark.slow
class TestDelftModelImpl:
def test_should_load_tensorflow_era_model_and_tag_every_token(
self, app_context: AppContext
):
model_impl = DelftModelImpl(HEADER_MODEL_URL, app_context)
data_generator = HeaderDataGenerator(DEFAULT_DOCUMENT_FEATURES_CONTEXT)
model_data_list = list(
data_generator.iter_model_data_for_layout_document(_get_layout_document())
)
data_lines = list(iter_data_lines_for_model_data_iterables([model_data_list]))
texts, features = load_data_crf_lines(data_lines)
tag_result = model_impl.predict_labels(
texts=texts.tolist(),
features=features.tolist(),
output_format=None
)
LOGGER.debug('tag_result: %r', tag_result)
assert len(tag_result) == 1
assert [token for token, _ in tag_result[0]] == list(texts[0])
preprocessor = model_impl.model.p
assert preprocessor is not None
assert preprocessor.indice_tag is not None
assert {label for _, label in tag_result[0]} <= set(preprocessor.indice_tag.values())
Loading
Loading