vlmparse 0.1.8__py3-none-any.whl → 0.1.10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
vlmparse/servers/utils.py CHANGED
@@ -1,228 +1,9 @@
1
- import getpass
2
- import time
3
- from contextlib import contextmanager
4
- from pathlib import Path
5
1
  from urllib.parse import parse_qsl, urlparse
6
2
 
7
3
  import docker
8
4
  from loguru import logger
9
5
 
10
6
 
11
- def _ensure_image_exists(
12
- client: docker.DockerClient,
13
- image: str,
14
- dockerfile_path: Path,
15
- ):
16
- """Check if image exists, build it if not."""
17
- try:
18
- client.images.get(image)
19
- logger.info(f"Docker image {image} found")
20
- return
21
- except docker.errors.ImageNotFound:
22
- logger.info(f"Docker image {image} not found, building...")
23
-
24
- if not dockerfile_path.exists():
25
- raise FileNotFoundError(
26
- f"Dockerfile directory not found at {dockerfile_path}"
27
- ) from None
28
-
29
- logger.info(f"Building image from {dockerfile_path}")
30
-
31
- # Use low-level API for real-time streaming
32
- api_client = docker.APIClient(base_url="unix://var/run/docker.sock")
33
-
34
- # Build the image with streaming
35
- build_stream = api_client.build(
36
- path=str(dockerfile_path),
37
- tag=image,
38
- rm=True,
39
- decode=True, # Automatically decode JSON responses to dict
40
- )
41
-
42
- # Stream build logs in real-time
43
- for chunk in build_stream:
44
- if "stream" in chunk:
45
- for line in chunk["stream"].splitlines():
46
- logger.info(line)
47
- elif "error" in chunk:
48
- logger.error(chunk["error"])
49
- raise docker.errors.BuildError(chunk["error"], build_stream) from None
50
- elif "status" in chunk:
51
- # Handle status updates (e.g., downloading layers)
52
- logger.debug(chunk["status"])
53
-
54
- logger.info(f"Successfully built image {image}")
55
-
56
-
57
- @contextmanager
58
- def docker_server(
59
- config: "DockerServerConfig", # noqa: F821
60
- timeout: int = 1000,
61
- cleanup: bool = True,
62
- ):
63
- """Generic context manager for Docker server deployment.
64
-
65
- Args:
66
- config: DockerServerConfig (can be VLLMDockerServerConfig or GenericDockerServerConfig)
67
- timeout: Timeout in seconds to wait for server to be ready
68
- cleanup: If True, stop and remove container on exit. If False, leave container running
69
-
70
- Yields:
71
- tuple: (base_url, container) - The base URL of the server and the Docker container object
72
- """
73
-
74
- client = docker.from_env()
75
- container = None
76
-
77
- try:
78
- # Ensure image exists
79
- logger.info(f"Checking for Docker image {config.docker_image}...")
80
-
81
- if config.dockerfile_dir is not None:
82
- _ensure_image_exists(
83
- client, config.docker_image, Path(config.dockerfile_dir)
84
- )
85
- else:
86
- # Pull pre-built image
87
- try:
88
- client.images.get(config.docker_image)
89
- logger.info(f"Docker image {config.docker_image} found locally")
90
- except docker.errors.ImageNotFound:
91
- logger.info(
92
- f"Docker image {config.docker_image} not found locally, pulling..."
93
- )
94
- client.images.pull(config.docker_image)
95
- logger.info(f"Successfully pulled {config.docker_image}")
96
-
97
- logger.info(
98
- f"Starting Docker container for {config.model_name} on port {config.docker_port}"
99
- )
100
-
101
- # Configure GPU access
102
- device_requests = None
103
-
104
- if config.gpu_device_ids is None:
105
- # Default: Try to use all GPUs if available
106
- device_requests = [
107
- docker.types.DeviceRequest(count=-1, capabilities=[["gpu"]])
108
- ]
109
- elif len(config.gpu_device_ids) > 0 and config.gpu_device_ids[0] != "":
110
- # Use specific GPU devices
111
- device_requests = [
112
- docker.types.DeviceRequest(
113
- device_ids=config.gpu_device_ids, capabilities=[["gpu"]]
114
- )
115
- ]
116
- else:
117
- # Empty list means CPU-only, no GPU
118
- device_requests = None
119
-
120
- # Use generic methods from config
121
- command = config.get_command()
122
- volumes = config.get_volumes()
123
- environment = config.get_environment()
124
- container_port = config.container_port
125
- log_prefix = config.model_name
126
-
127
- # Construct URI for label
128
- uri = f"http://localhost:{config.docker_port}{config.get_base_url_suffix()}"
129
-
130
- # Determine GPU label
131
- if config.gpu_device_ids is None:
132
- gpu_label = "all"
133
- elif len(config.gpu_device_ids) == 0 or (
134
- len(config.gpu_device_ids) == 1 and config.gpu_device_ids[0] == ""
135
- ):
136
- gpu_label = "cpu"
137
- else:
138
- gpu_label = ",".join(config.gpu_device_ids)
139
-
140
- # Start container
141
- container_kwargs = {
142
- "image": config.docker_image,
143
- "ports": {f"{container_port}/tcp": config.docker_port},
144
- "detach": True,
145
- "remove": True,
146
- "name": f"vlmparse-{config.model_name.replace('/', '-')}-{getpass.getuser()}",
147
- "labels": {
148
- "vlmparse_model_name": config.model_name,
149
- "vlmparse_uri": uri,
150
- "vlmparse_gpus": gpu_label,
151
- },
152
- }
153
-
154
- if device_requests is not None:
155
- container_kwargs["device_requests"] = device_requests
156
- if command:
157
- container_kwargs["command"] = command
158
- if environment:
159
- container_kwargs["environment"] = environment
160
- if volumes:
161
- container_kwargs["volumes"] = volumes
162
- if config.entrypoint:
163
- container_kwargs["entrypoint"] = config.entrypoint
164
-
165
- container = client.containers.run(**container_kwargs)
166
-
167
- logger.info(
168
- f"Container {container.short_id} started, waiting for server to be ready..."
169
- )
170
-
171
- # Wait for server to be ready
172
- start_time = time.time()
173
- server_ready = False
174
- last_log_position = 0
175
-
176
- while time.time() - start_time < timeout:
177
- try:
178
- container.reload()
179
- except docker.errors.NotFound as e:
180
- logger.error("Container stopped unexpectedly during startup")
181
- raise RuntimeError(
182
- "Container crashed during initialization. Check Docker logs for details."
183
- ) from e
184
-
185
- if container.status == "running":
186
- # Get all logs and display new ones
187
- all_logs = container.logs().decode("utf-8")
188
-
189
- # Display new log lines
190
- if len(all_logs) > last_log_position:
191
- new_logs = all_logs[last_log_position:]
192
- for line in new_logs.splitlines():
193
- if line.strip(): # Only print non-empty lines
194
- logger.info(f"[{log_prefix}] {line}")
195
- last_log_position = len(all_logs)
196
-
197
- # Check if server is ready
198
- for indicator in config.server_ready_indicators:
199
- if indicator in all_logs:
200
- server_ready = True
201
- if server_ready:
202
- logger.info(f"Server ready indicator '{indicator}' found in logs")
203
- break
204
-
205
- time.sleep(2)
206
-
207
- if not server_ready:
208
- raise TimeoutError(f"Server did not become ready within {timeout} seconds")
209
-
210
- # Build base URL using config's suffix method
211
- base_url = (
212
- f"http://localhost:{config.docker_port}{config.get_base_url_suffix()}"
213
- )
214
-
215
- logger.info(f"{log_prefix} server ready at {base_url}")
216
-
217
- yield base_url, container
218
-
219
- finally:
220
- if cleanup and container:
221
- logger.info(f"Stopping container {container.short_id}")
222
- container.stop(timeout=10)
223
- logger.info("Container stopped")
224
-
225
-
226
7
  def normalize_uri(uri: str) -> tuple:
227
8
  u = urlparse(uri)
228
9
 
@@ -277,3 +58,86 @@ def get_model_from_uri(uri: str) -> str:
277
58
  if model is None:
278
59
  raise ValueError(f"No model found for URI {uri}")
279
60
  return model
61
+
62
+
63
+ def _get_container_labels(container) -> dict[str, str]:
64
+ labels: dict[str, str] = {}
65
+ try:
66
+ labels.update(getattr(container, "labels", None) or {})
67
+ except Exception:
68
+ pass
69
+
70
+ try:
71
+ labels.update((container.attrs or {}).get("Config", {}).get("Labels", {}) or {})
72
+ except Exception:
73
+ pass
74
+
75
+ return labels
76
+
77
+
78
+ def _stop_compose_stack_for_container(target_container) -> bool:
79
+ """If container belongs to a docker-compose project, stop+remove the whole stack.
80
+
81
+ Returns True if a compose stack was detected and a stack stop was attempted.
82
+ """
83
+
84
+ import subprocess
85
+
86
+ labels = _get_container_labels(target_container)
87
+
88
+ project = labels.get("com.docker.compose.project") or labels.get(
89
+ "vlmparse_compose_project"
90
+ )
91
+ compose_file = labels.get("vlmparse_compose_file")
92
+
93
+ if not project:
94
+ return False
95
+
96
+ # Preferred: docker compose down (stops + removes all services/networks consistently)
97
+ if compose_file:
98
+ logger.info(
99
+ f"Detected docker-compose project '{project}'. Bringing stack down (stop + remove)..."
100
+ )
101
+ subprocess.run(
102
+ [
103
+ "docker",
104
+ "compose",
105
+ "-f",
106
+ compose_file,
107
+ "--project-name",
108
+ project,
109
+ "down",
110
+ "--remove-orphans",
111
+ ],
112
+ check=False,
113
+ capture_output=True,
114
+ text=True,
115
+ )
116
+ logger.info("✓ Compose stack brought down")
117
+ return True
118
+
119
+ # Fallback: remove all containers in the compose project via Docker labels
120
+ import docker
121
+
122
+ logger.info(
123
+ f"Detected docker-compose project '{project}' (compose file unknown). "
124
+ "Stopping + removing all project containers via Docker API..."
125
+ )
126
+ client = docker.from_env()
127
+ containers = client.containers.list(
128
+ all=True, filters={"label": [f"com.docker.compose.project={project}"]}
129
+ )
130
+ for c in containers:
131
+ try:
132
+ c.stop()
133
+ except Exception:
134
+ pass
135
+ try:
136
+ c.remove(force=True)
137
+ except Exception:
138
+ pass
139
+
140
+ logger.info(
141
+ f"✓ Removed {len(containers)} container(s) from compose project '{project}'"
142
+ )
143
+ return True
@@ -40,7 +40,7 @@ def run_streamlit(folder: str) -> None:
40
40
  col1, col2 = st.columns(2)
41
41
  with col1:
42
42
  with st.container(height=700):
43
- st.write(doc.pages[settings["page_no"]].text)
43
+ st.markdown(doc.pages[settings["page_no"]].text, unsafe_allow_html=True)
44
44
 
45
45
  with col2:
46
46
  if settings["plot_layouts"]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vlmparse
3
- Version: 0.1.8
3
+ Version: 0.1.10
4
4
  Requires-Python: >=3.11.0
5
5
  Description-Content-Type: text/markdown
6
6
  License-File: LICENSE
@@ -56,7 +56,7 @@ Dynamic: license-file
56
56
 
57
57
  <div align="center">
58
58
 
59
- [\[📜 arXiv coming soon\]] | [[Dataset (🤗Hugging Face)]](https://huggingface.co/datasets/pulsia/fr-bench-pdf2md) | [[pypi]](https://pypi.org/project/vlmparse/) | [[vlmparse]](https://github.com/ld-lab-pulsia/vlmparse) | [[Benchmark]](https://github.com/ld-lab-pulsia/benchpdf2md)
59
+ [\[📜 arXiv coming soon\]] | [[Dataset (🤗Hugging Face)]](https://huggingface.co/datasets/pulsia/fr-bench-pdf2md) | [[pypi]](https://pypi.org/project/vlmparse/) | [[vlmparse]](https://github.com/ld-lab-pulsia/vlmparse) | [[Benchmark]](https://github.com/ld-lab-pulsia/benchpdf2md) | [[Leaderboard]](https://huggingface.co/spaces/pulsia/fr-bench-pdf2md)
60
60
 
61
61
  </div>
62
62
 
@@ -71,7 +71,7 @@ Features:
71
71
 
72
72
  Supported Converters:
73
73
 
74
- - **Open Source Small VLMs**: `lightonocr`, `mineru2.5`, `hunyuanocr`, `paddleocrvl`, `granite-docling`, `olmocr2-fp8`, `dotsocr`, `chandra`, `deepseekocr`, `nanonets/Nanonets-OCR2-3B`
74
+ - **Open Source Small VLMs**: `lightonocr2`, `mineru2.5`, `hunyuanocr`, `paddleocrvl-1.5`, `granite-docling`, `olmocr2-fp8`, `dotsocr`, `chandra`, `deepseekocr2`, `nanonets/Nanonets-OCR2-3B`
75
75
  - **Open Source Generalist VLMs**: such as the Qwen family.
76
76
  - **Pipelines**: `docling`
77
77
  - **Proprietary LLMs**: `gemini`, `gpt`
@@ -115,13 +115,13 @@ Note that you can bypass the previous installation step and just add uvx before
115
115
  With a general VLM (requires setting your api key as an environment variable):
116
116
 
117
117
  ```bash
118
- vlmparse convert --input "*.pdf" --out_folder ./output --model gemini-2.5-flash-lite
118
+ vlmparse convert "*.pdf" --out_folder ./output --model gemini-2.5-flash-lite
119
119
  ```
120
120
 
121
121
  Convert with auto deployment of a small vlm (or any huggingface VLM model, requires a gpu + docker installation):
122
122
 
123
123
  ```bash
124
- vlmparse convert --input "*.pdf" --out_folder ./output --model nanonets/Nanonets-OCR2-3B
124
+ vlmparse convert "*.pdf" --out_folder ./output --model nanonets/Nanonets-OCR2-3B
125
125
  ```
126
126
 
127
127
  ### Deploy a local model server
@@ -131,13 +131,13 @@ Deployment (requires a gpu + docker installation):
131
131
  - Check that the port is not used by another service.
132
132
 
133
133
  ```bash
134
- vlmparse serve --model lightonocr --port 8000 --gpus 1
134
+ vlmparse serve lightonocr2 --port 8000 --gpus 1
135
135
  ```
136
136
 
137
137
  then convert:
138
138
 
139
139
  ```bash
140
- vlmparse convert --input "*.pdf" --out_folder ./output --model lightonocr --uri http://localhost:8000/v1
140
+ vlmparse convert "*.pdf" --out_folder ./output --uri http://localhost:8000/v1
141
141
  ```
142
142
 
143
143
  You can also list all running servers:
@@ -1,15 +1,16 @@
1
1
  vlmparse/base_model.py,sha256=4U4UPe8SNArliKnUf8pp8zQugWYsnhg9okylt7mrW1U,381
2
2
  vlmparse/build_doc.py,sha256=fb7awoqVN-6NBlKVkMFb1v1iTWcxne5QAyNaKYTyvM4,2275
3
- vlmparse/cli.py,sha256=asew0JdpbgFZrZqnG-Bqh5A_DrXcP0XomLB3y3AgG6Y,12855
3
+ vlmparse/cli.py,sha256=jP_BnFaeW1rm3iTcdw5WFRfQUgDYd6HC1Zh-5JbE9_4,18285
4
4
  vlmparse/constants.py,sha256=DYaK7KtTW8p9MPb3iPvoP5H1r7ICRuIFo89P01q4uCI,184
5
5
  vlmparse/converter.py,sha256=KKcXqrp3nJo3d7DXjHn3O2SklbsJ489rDY4NJ9O42Fs,8795
6
- vlmparse/converter_with_server.py,sha256=A84l3YNal-hs2mMlER1sB29rddsO8MNOP2j9ts0ujtE,7280
7
- vlmparse/registries.py,sha256=B4kxibP7XYbhL9bZ5gn21LQCPhHCYftAM4i0-xD9fRs,6469
6
+ vlmparse/converter_with_server.py,sha256=nDGF-FEqskAECam_Sm8GbPMGPdI2Iua4lHaHbpMZx_k,8872
7
+ vlmparse/registries.py,sha256=4xiDKyIzAW68ZWyOtUmBOvzcXVqTPPdeoxD2s9RbjZ0,6714
8
8
  vlmparse/utils.py,sha256=6Ff9OfAIVR-4_37QD5sifoNt_GmB3YUqgFwmIjuemtc,1727
9
9
  vlmparse/clients/chandra.py,sha256=zAHjgI_MJ5FVGANHCG8KJQByaw6-zTS6CHXsCBA8TJI,13025
10
- vlmparse/clients/deepseekocr.py,sha256=pKdNJD9v86BRn7YrXE6PGk_jQxnbZ_6UjgSUxgd3Su4,6859
10
+ vlmparse/clients/deepseekocr.py,sha256=4NiW-the4JHPqI0rNF2xG3juGZJX4tiI7doCk1jyYec,12772
11
11
  vlmparse/clients/docling.py,sha256=BLtNAxVJR6qvPip4ZBP-se8IMNFSbJ-fWEGlTSwimK8,5310
12
12
  vlmparse/clients/dotsocr.py,sha256=oAUzDMTObeW0sTy5sFl08O6GQPSTic5ITbJYh_45Z54,10414
13
+ vlmparse/clients/glmocr.py,sha256=WzRntPKx3BPDAZEjfAHQj2OukBEEOEKbtpZag0me53g,7727
13
14
  vlmparse/clients/granite_docling.py,sha256=KYaEdgk3oD0TuYDKqTQ4o6IkXC-E3AIYJ2KYxnEsjWM,3595
14
15
  vlmparse/clients/hunyuanocr.py,sha256=etpIiA28OoGW-o5pOGeBxOlUDjUQ4zcKXWnJ8ba44DU,1979
15
16
  vlmparse/clients/lightonocr.py,sha256=ZWC12U6myDr_2EuOPYGyJYxpBachjOUtLrxS62A8mzg,2048
@@ -18,21 +19,26 @@ vlmparse/clients/mistral_converter.py,sha256=_hEyK_2vM5LEwbt30bFodMrWJtavLsBDxCk
18
19
  vlmparse/clients/nanonetocr.py,sha256=gTbD4OtuHiWd6Ack6Bx-anZM9P_aErfSHXwtymETvqM,1665
19
20
  vlmparse/clients/olmocr.py,sha256=V4638WftLCTr5Q6ZRgWKKSPAhFYdpBw3izeuda6EKDQ,1966
20
21
  vlmparse/clients/openai_converter.py,sha256=bckm33Pkvqul--DjfEEEI3evn4_va0CoQcigdpCCMGc,7746
21
- vlmparse/clients/paddleocrvl.py,sha256=q3AgEWj0UyXGpSEVZISdfqv2PV_qY-uF498bL8U1tpg,1596
22
+ vlmparse/clients/paddleocrvl.py,sha256=50HuQm5aOH7xi0vdJ5isowsw6eBtEBNV8DWY4wbaTGA,7094
22
23
  vlmparse/clients/prompts.py,sha256=-J60lqxgRzlkQ9VsQLxmWsIMaDt-gNqWqWoqHIw9CLc,4228
23
24
  vlmparse/clients/pipe_utils/cleaner.py,sha256=oxBkBTOkluN1lmeNbzajRIe0_D__ZGwUOBaI_Ph0uxE,2396
24
25
  vlmparse/clients/pipe_utils/html_to_md_conversion.py,sha256=cFFqzD2jCNw_968_eu3Wt--Ox7iJj2Rn5UoP_DZWosU,4112
25
26
  vlmparse/clients/pipe_utils/utils.py,sha256=935ecIO446I0pstszE_1nrIPHn1Ffrxunq7fVd0dsd8,315
26
27
  vlmparse/data_model/box.py,sha256=lJsh4qhjgYXZF5vTSJ1qMXD5GVlBi2_SBedBMlfJikU,16868
27
28
  vlmparse/data_model/document.py,sha256=xheaMeStOj2c9GZKmdtxcEl_Dj44V5JyVp6JnTrSpH0,4615
28
- vlmparse/servers/docker_server.py,sha256=FOIHU0_CDfyZ9UA285BrnUFuEMJRxbu-OzlByBa-P9s,7951
29
+ vlmparse/servers/base_server.py,sha256=NLoGaXu8tapAm2dKjzZSBNY0EOXAK4Zo-zMtTx_VvdA,4343
30
+ vlmparse/servers/docker_compose_deployment.py,sha256=sGmZd7TRkOwZyh8raZhE2udxCBhKhxyn0hJcjCn9_DE,16999
31
+ vlmparse/servers/docker_compose_server.py,sha256=UsAokpmGeTHaaS6MVVlM3KxpILDUzeWuS4GSlr_2gT0,1394
32
+ vlmparse/servers/docker_run_deployment.py,sha256=34vHVZwRcw2Z5i5kPj8r2GCc7OmgVJXaSPctlXDWgvU,7898
33
+ vlmparse/servers/docker_server.py,sha256=KrTTcwN1wxhK0bpMuZtJZ5cQz4MuaTWLTlh9nMG87Fg,3777
29
34
  vlmparse/servers/model_identity.py,sha256=DkH7KQAAZA9Sn7eJEnaKfH54XSEI17aqD1ScqqkTBEk,1711
30
- vlmparse/servers/utils.py,sha256=tIXhgbF9EVOJy2nYEguVq69gn9ATxtya_1F4wZSt68o,9454
35
+ vlmparse/servers/server_registry.py,sha256=FUF_XnN8872vKnc8-TrEBntwBS5i3ZYVJvTHrHfI7IM,1315
36
+ vlmparse/servers/utils.py,sha256=rbqn9i6XB1YOEFluP4Ur0Ma40_6riUxJ1eMS8LSWbKs,3998
31
37
  vlmparse/st_viewer/fs_nav.py,sha256=7GNH68h2Loh5pQ64Pe72-D2cs2BLhqRXevEmKdFmPX0,1616
32
- vlmparse/st_viewer/st_viewer.py,sha256=m2rQTtk5rlwErNmivNAg-4rkHkvNkvLhoJZxFQi7Dwk,2105
33
- vlmparse-0.1.8.dist-info/licenses/LICENSE,sha256=3TKJHk8hPBR5dbLWZ3IpfCftl-_m-iyBwpYQGZYxj14,1080
34
- vlmparse-0.1.8.dist-info/METADATA,sha256=dwu5tiTLuhVMYL-ZQCMNYW_MNlJu84V2us0aeRfrSpU,6048
35
- vlmparse-0.1.8.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
36
- vlmparse-0.1.8.dist-info/entry_points.txt,sha256=gD5berP6HwE2wNIkls-Lw5goiceA8uMgPEd7ifnFJXs,47
37
- vlmparse-0.1.8.dist-info/top_level.txt,sha256=k4ni-GNH_iAX7liQEsk_KY_c3xgZgt8k9fsSs9IXLXs,9
38
- vlmparse-0.1.8.dist-info/RECORD,,
38
+ vlmparse/st_viewer/st_viewer.py,sha256=wg0qfhAKdvnkpc3xDK8QnWP9adjEThzeS-I5vHGDhIU,2132
39
+ vlmparse-0.1.10.dist-info/licenses/LICENSE,sha256=3TKJHk8hPBR5dbLWZ3IpfCftl-_m-iyBwpYQGZYxj14,1080
40
+ vlmparse-0.1.10.dist-info/METADATA,sha256=OIRlJUlRioNzrehJIK2dmBcTFHI7A6H5uedu-EzDTQA,6077
41
+ vlmparse-0.1.10.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
42
+ vlmparse-0.1.10.dist-info/entry_points.txt,sha256=gD5berP6HwE2wNIkls-Lw5goiceA8uMgPEd7ifnFJXs,47
43
+ vlmparse-0.1.10.dist-info/top_level.txt,sha256=k4ni-GNH_iAX7liQEsk_KY_c3xgZgt8k9fsSs9IXLXs,9
44
+ vlmparse-0.1.10.dist-info/RECORD,,