vlmparse 0.1.7__py3-none-any.whl → 0.1.9__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.
Files changed (36) hide show
  1. vlmparse/build_doc.py +20 -19
  2. vlmparse/cli.py +439 -270
  3. vlmparse/clients/chandra.py +176 -60
  4. vlmparse/clients/deepseekocr.py +193 -12
  5. vlmparse/clients/docling.py +0 -1
  6. vlmparse/clients/dotsocr.py +34 -31
  7. vlmparse/clients/glmocr.py +243 -0
  8. vlmparse/clients/granite_docling.py +9 -36
  9. vlmparse/clients/hunyuanocr.py +5 -1
  10. vlmparse/clients/lightonocr.py +23 -1
  11. vlmparse/clients/mineru.py +0 -1
  12. vlmparse/clients/mistral_converter.py +85 -0
  13. vlmparse/clients/nanonetocr.py +5 -1
  14. vlmparse/clients/olmocr.py +6 -2
  15. vlmparse/clients/openai_converter.py +95 -60
  16. vlmparse/clients/paddleocrvl.py +195 -40
  17. vlmparse/converter.py +51 -11
  18. vlmparse/converter_with_server.py +92 -19
  19. vlmparse/registries.py +107 -89
  20. vlmparse/servers/base_server.py +127 -0
  21. vlmparse/servers/docker_compose_deployment.py +489 -0
  22. vlmparse/servers/docker_compose_server.py +39 -0
  23. vlmparse/servers/docker_run_deployment.py +226 -0
  24. vlmparse/servers/docker_server.py +17 -109
  25. vlmparse/servers/model_identity.py +48 -0
  26. vlmparse/servers/server_registry.py +42 -0
  27. vlmparse/servers/utils.py +83 -219
  28. vlmparse/st_viewer/st_viewer.py +1 -1
  29. vlmparse/utils.py +15 -2
  30. {vlmparse-0.1.7.dist-info → vlmparse-0.1.9.dist-info}/METADATA +13 -3
  31. vlmparse-0.1.9.dist-info/RECORD +44 -0
  32. {vlmparse-0.1.7.dist-info → vlmparse-0.1.9.dist-info}/WHEEL +1 -1
  33. vlmparse-0.1.7.dist-info/RECORD +0 -36
  34. {vlmparse-0.1.7.dist-info → vlmparse-0.1.9.dist-info}/entry_points.txt +0 -0
  35. {vlmparse-0.1.7.dist-info → vlmparse-0.1.9.dist-info}/licenses/LICENSE +0 -0
  36. {vlmparse-0.1.7.dist-info → vlmparse-0.1.9.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,226 @@
1
+ import getpass
2
+ import time
3
+ from contextlib import contextmanager
4
+ from pathlib import Path
5
+ from typing import TYPE_CHECKING
6
+
7
+ import docker
8
+ from loguru import logger
9
+
10
+ if TYPE_CHECKING:
11
+ from .base_server import BaseServerConfig
12
+
13
+
14
+ def _ensure_image_exists(
15
+ client: docker.DockerClient,
16
+ image: str,
17
+ dockerfile_path: Path,
18
+ ):
19
+ """Check if image exists, build it if not."""
20
+ try:
21
+ client.images.get(image)
22
+ logger.info(f"Docker image {image} found")
23
+ return
24
+ except docker.errors.ImageNotFound:
25
+ logger.info(f"Docker image {image} not found, building...")
26
+
27
+ if not dockerfile_path.exists():
28
+ raise FileNotFoundError(
29
+ f"Dockerfile directory not found at {dockerfile_path}"
30
+ ) from None
31
+
32
+ logger.info(f"Building image from {dockerfile_path}")
33
+
34
+ # Use low-level API for real-time streaming
35
+ api_client = docker.APIClient(base_url="unix://var/run/docker.sock")
36
+
37
+ # Build the image with streaming
38
+ build_stream = api_client.build(
39
+ path=str(dockerfile_path),
40
+ tag=image,
41
+ rm=True,
42
+ decode=True, # Automatically decode JSON responses to dict
43
+ )
44
+
45
+ # Stream build logs in real-time
46
+ for chunk in build_stream:
47
+ if "stream" in chunk:
48
+ for line in chunk["stream"].splitlines():
49
+ logger.info(line)
50
+ elif "error" in chunk:
51
+ logger.error(chunk["error"])
52
+ raise docker.errors.BuildError(chunk["error"], build_stream) from None
53
+ elif "status" in chunk:
54
+ # Handle status updates (e.g., downloading layers)
55
+ logger.debug(chunk["status"])
56
+
57
+ logger.info(f"Successfully built image {image}")
58
+
59
+
60
+ @contextmanager
61
+ def docker_server(
62
+ config: "BaseServerConfig",
63
+ timeout: int = 1000,
64
+ cleanup: bool = True,
65
+ ):
66
+ """Generic context manager for Docker server deployment.
67
+
68
+ Args:
69
+ config: BaseServerConfig (can be any subclass like DockerServerConfig or VLLMDockerServerConfig)
70
+ timeout: Timeout in seconds to wait for server to be ready
71
+ cleanup: If True, stop and remove container on exit. If False, leave container running
72
+
73
+ Yields:
74
+ tuple: (base_url, container) - The base URL of the server and the Docker container object
75
+ """
76
+
77
+ client = docker.from_env()
78
+ container = None
79
+
80
+ try:
81
+ # Ensure image exists
82
+ logger.info(f"Checking for Docker image {config.docker_image}...")
83
+
84
+ if config.dockerfile_dir is not None:
85
+ _ensure_image_exists(
86
+ client, config.docker_image, Path(config.dockerfile_dir)
87
+ )
88
+ else:
89
+ # Pull pre-built image
90
+ try:
91
+ client.images.get(config.docker_image)
92
+ logger.info(f"Docker image {config.docker_image} found locally")
93
+ except docker.errors.ImageNotFound:
94
+ logger.info(
95
+ f"Docker image {config.docker_image} not found locally, pulling..."
96
+ )
97
+ client.images.pull(config.docker_image)
98
+ logger.info(f"Successfully pulled {config.docker_image}")
99
+
100
+ logger.info(
101
+ f"Starting Docker container for {config.model_name} on port {config.docker_port}"
102
+ )
103
+
104
+ # Configure GPU access
105
+ device_requests = None
106
+
107
+ if config.gpu_device_ids is None:
108
+ # Default: Try to use all GPUs if available
109
+ device_requests = [
110
+ docker.types.DeviceRequest(count=-1, capabilities=[["gpu"]])
111
+ ]
112
+ elif len(config.gpu_device_ids) > 0 and config.gpu_device_ids[0] != "":
113
+ # Use specific GPU devices
114
+ device_requests = [
115
+ docker.types.DeviceRequest(
116
+ device_ids=config.gpu_device_ids, capabilities=[["gpu"]]
117
+ )
118
+ ]
119
+ else:
120
+ # Empty list means CPU-only, no GPU
121
+ device_requests = None
122
+
123
+ # Use generic methods from config
124
+ command = config.get_command()
125
+ volumes = config.get_volumes()
126
+ environment = config.get_environment()
127
+ container_port = config.container_port
128
+ log_prefix = config.model_name
129
+
130
+ # Construct URI for label
131
+ uri = f"http://localhost:{config.docker_port}{config.get_base_url_suffix()}"
132
+
133
+ # Determine GPU label
134
+ if config.gpu_device_ids is None:
135
+ gpu_label = "0"
136
+ elif len(config.gpu_device_ids) == 0 or (
137
+ len(config.gpu_device_ids) == 1 and config.gpu_device_ids[0] == ""
138
+ ):
139
+ gpu_label = "cpu"
140
+ else:
141
+ gpu_label = ",".join(config.gpu_device_ids)
142
+
143
+ # Start container
144
+ container_kwargs = {
145
+ "image": config.docker_image,
146
+ "ports": {f"{container_port}/tcp": config.docker_port},
147
+ "detach": True,
148
+ "remove": True,
149
+ "name": f"vlmparse-{config.model_name.replace('/', '-')}-{getpass.getuser()}",
150
+ "labels": {
151
+ "vlmparse_model_name": config.model_name,
152
+ "vlmparse_uri": uri,
153
+ "vlmparse_gpus": gpu_label,
154
+ },
155
+ }
156
+
157
+ if device_requests is not None:
158
+ container_kwargs["device_requests"] = device_requests
159
+ if command:
160
+ container_kwargs["command"] = command
161
+ if environment:
162
+ container_kwargs["environment"] = environment
163
+ if volumes:
164
+ container_kwargs["volumes"] = volumes
165
+ if config.entrypoint:
166
+ container_kwargs["entrypoint"] = config.entrypoint
167
+
168
+ container = client.containers.run(**container_kwargs)
169
+
170
+ logger.info(
171
+ f"Container {container.short_id} started, waiting for server to be ready..."
172
+ )
173
+
174
+ # Wait for server to be ready
175
+ start_time = time.time()
176
+ server_ready = False
177
+ last_log_position = 0
178
+
179
+ while time.time() - start_time < timeout:
180
+ try:
181
+ container.reload()
182
+ except docker.errors.NotFound as e:
183
+ logger.error("Container stopped unexpectedly during startup")
184
+ raise RuntimeError(
185
+ "Container crashed during initialization. Check Docker logs for details."
186
+ ) from e
187
+
188
+ if container.status == "running":
189
+ # Get all logs and display new ones
190
+ all_logs = container.logs().decode("utf-8")
191
+
192
+ # Display new log lines
193
+ if len(all_logs) > last_log_position:
194
+ new_logs = all_logs[last_log_position:]
195
+ for line in new_logs.splitlines():
196
+ if line.strip(): # Only print non-empty lines
197
+ logger.info(f"[{log_prefix}] {line}")
198
+ last_log_position = len(all_logs)
199
+
200
+ # Check if server is ready
201
+ for indicator in config.server_ready_indicators:
202
+ if indicator in all_logs:
203
+ server_ready = True
204
+ if server_ready:
205
+ logger.info(f"Server ready indicator '{indicator}' found in logs")
206
+ break
207
+
208
+ time.sleep(2)
209
+
210
+ if not server_ready:
211
+ raise TimeoutError(f"Server did not become ready within {timeout} seconds")
212
+
213
+ # Build base URL using config's suffix method
214
+ base_url = (
215
+ f"http://localhost:{config.docker_port}{config.get_base_url_suffix()}"
216
+ )
217
+
218
+ logger.info(f"{log_prefix} server ready at {base_url}")
219
+
220
+ yield base_url, container
221
+
222
+ finally:
223
+ if cleanup and container:
224
+ logger.info(f"Stopping container {container.short_id}")
225
+ container.stop(timeout=10)
226
+ logger.info("Container stopped")
@@ -1,45 +1,28 @@
1
1
  import os
2
- from typing import Callable
3
2
 
4
- from loguru import logger
5
- from pydantic import BaseModel, Field
3
+ from pydantic import Field
6
4
 
7
- from .utils import docker_server
5
+ from .base_server import BaseServer, BaseServerConfig
6
+ from .docker_run_deployment import docker_server
8
7
 
9
8
 
10
- class DockerServerConfig(BaseModel):
11
- """Base configuration for deploying a Docker server."""
9
+ class DockerServerConfig(BaseServerConfig):
10
+ """Configuration for deploying a Docker server.
11
+
12
+ Inherits from BaseServerConfig which provides common server configuration.
13
+ """
12
14
 
13
- model_name: str
14
15
  docker_image: str
15
16
  dockerfile_dir: str | None = None
16
17
  command_args: list[str] = Field(default_factory=list)
17
- server_ready_indicators: list[str] = Field(
18
- default_factory=lambda: [
19
- "Application startup complete",
20
- "Uvicorn running",
21
- "Starting vLLM API server",
22
- ]
23
- )
24
- docker_port: int = 8056
25
- gpu_device_ids: list[str] | None = None
26
- container_port: int = 8000
27
- environment: dict[str, str] = Field(default_factory=dict)
28
18
  volumes: dict[str, dict] | None = None
29
19
  entrypoint: str | None = None
30
- aliases: list[str] = Field(default_factory=list)
31
-
32
- class Config:
33
- extra = "allow"
34
20
 
35
21
  @property
36
22
  def client_config(self):
37
23
  """Override in subclasses to return appropriate client config."""
38
24
  raise NotImplementedError
39
25
 
40
- def get_client(self, **kwargs):
41
- return self.client_config.get_client(**kwargs)
42
-
43
26
  def get_server(self, auto_stop: bool = True):
44
27
  return ConverterServer(config=self, auto_stop=auto_stop)
45
28
 
@@ -64,14 +47,6 @@ class DockerServerConfig(BaseModel):
64
47
  """Setup volumes for container. Override in subclasses for specific logic."""
65
48
  return self.volumes
66
49
 
67
- def get_environment(self) -> dict | None:
68
- """Setup environment variables. Override in subclasses for specific logic."""
69
- return self.environment if self.environment else None
70
-
71
- def get_base_url_suffix(self) -> str:
72
- """Return URL suffix (e.g., '/v1' for OpenAI-compatible APIs). Override in subclasses."""
73
- return ""
74
-
75
50
 
76
51
  DEFAULT_MODEL_NAME = "vllm-model"
77
52
 
@@ -84,22 +59,16 @@ class VLLMDockerServerConfig(DockerServerConfig):
84
59
  hf_home_folder: str | None = os.getenv("HF_HOME", None)
85
60
  add_model_key_to_server: bool = False
86
61
  container_port: int = 8000
87
- aliases: list[str] = Field(default_factory=list)
88
-
89
- @property
90
- def llm_params(self):
91
- from vlmparse.clients.openai_converter import LLMParams
92
-
93
- return LLMParams(
94
- base_url=f"http://localhost:{self.docker_port}{self.get_base_url_suffix()}",
95
- model_name=self.default_model_name,
96
- )
97
62
 
98
63
  @property
99
64
  def client_config(self):
100
65
  from vlmparse.clients.openai_converter import OpenAIConverterConfig
101
66
 
102
- return OpenAIConverterConfig(llm_params=self.llm_params)
67
+ return OpenAIConverterConfig(
68
+ **self._create_client_kwargs(
69
+ f"http://localhost:{self.docker_port}{self.get_base_url_suffix()}"
70
+ )
71
+ )
103
72
 
104
73
  def get_command(self) -> list[str]:
105
74
  """Build VLLM-specific command."""
@@ -143,70 +112,9 @@ class VLLMDockerServerConfig(DockerServerConfig):
143
112
  return "/v1"
144
113
 
145
114
 
146
- class ConverterServer:
115
+ class ConverterServer(BaseServer):
147
116
  """Manages Docker server lifecycle with start/stop methods."""
148
117
 
149
- def __init__(self, config: DockerServerConfig, auto_stop: bool = True):
150
- self.config = config
151
- self.auto_stop = auto_stop
152
- self._server_context = None
153
- self._container = None
154
- self.base_url = None
155
-
156
- def start(self):
157
- """Start the Docker server."""
158
- if self._server_context is not None:
159
- logger.warning("Server already started")
160
- return self.base_url, self._container
161
-
162
- # Use the generic docker_server for all server types
163
- self._server_context = docker_server(config=self.config, cleanup=self.auto_stop)
164
-
165
- self.base_url, self._container = self._server_context.__enter__()
166
- logger.info(f"Server started at {self.base_url}")
167
- logger.info(f"Container ID: {self._container.id}")
168
- logger.info(f"Container name: {self._container.name}")
169
- return self.base_url, self._container
170
-
171
- def stop(self):
172
- """Stop the Docker server."""
173
- if self._server_context is not None:
174
- self._server_context.__exit__(None, None, None)
175
- self._server_context = None
176
- self._container = None
177
- self.base_url = None
178
- logger.info("Server stopped")
179
-
180
- def __del__(self):
181
- """Automatically stop server when object is destroyed if auto_stop is True."""
182
- if self.auto_stop and self._server_context is not None:
183
- self.stop()
184
-
185
-
186
- class DockerConfigRegistry:
187
- """Registry for mapping model names to their Docker configurations."""
188
-
189
- def __init__(self):
190
- self._registry = dict()
191
-
192
- def register(
193
- self, model_name: str, config_factory: Callable[[], DockerServerConfig | None]
194
- ):
195
- """Register a config factory for a model name."""
196
- self._registry[model_name] = config_factory
197
-
198
- def get(self, model_name: str, default=False) -> DockerServerConfig | None:
199
- """Get config for a model name. Returns default if not registered."""
200
- if model_name not in self._registry:
201
- if default:
202
- return VLLMDockerServerConfig(model_name=model_name)
203
- return None
204
- return self._registry[model_name]()
205
-
206
- def list_models(self) -> list[str]:
207
- """List all registered model names."""
208
- return list(self._registry.keys())
209
-
210
-
211
- # Global registry instance
212
- docker_config_registry = DockerConfigRegistry()
118
+ def _create_server_context(self):
119
+ """Create the Docker server context."""
120
+ return docker_server(config=self.config, cleanup=self.auto_stop)
@@ -0,0 +1,48 @@
1
+ """Model identity mixin for consistent model name handling between server and client configs."""
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class ModelIdentityMixin(BaseModel):
7
+ """Mixin providing model identity fields with validation.
8
+
9
+ This mixin ensures that model_name and default_model_name are consistently
10
+ passed from server configs to client configs.
11
+ """
12
+
13
+ model_name: str
14
+ default_model_name: str | None = None
15
+ aliases: list[str] = Field(default_factory=list)
16
+
17
+ def get_effective_model_name(self) -> str:
18
+ """Returns the model name to use for API calls."""
19
+ return self.default_model_name if self.default_model_name else self.model_name
20
+
21
+ def _create_client_kwargs(self, base_url: str) -> dict:
22
+ """Generate kwargs for client config with model identity.
23
+
24
+ Use this method in server configs to ensure consistent passing
25
+ of model_name and default_model_name to client configs.
26
+
27
+ Args:
28
+ base_url: The base URL for the client to connect to.
29
+
30
+ Returns:
31
+ Dictionary with base_url, model_name, and default_model_name.
32
+ """
33
+ return {
34
+ "base_url": base_url,
35
+ "model_name": self.model_name,
36
+ "default_model_name": self.get_effective_model_name(),
37
+ }
38
+
39
+ def get_all_names(self) -> list[str]:
40
+ """Get all names this model can be referenced by.
41
+
42
+ Returns:
43
+ List containing model_name, aliases, and short name (after last /).
44
+ """
45
+ names = [self.model_name] + self.aliases
46
+ if "/" in self.model_name:
47
+ names.append(self.model_name.split("/")[-1])
48
+ return [n for n in names if isinstance(n, str)]
@@ -0,0 +1,42 @@
1
+ from typing import Callable
2
+
3
+ from vlmparse.servers.base_server import BaseServerConfig
4
+
5
+
6
+ class DockerConfigRegistry:
7
+ """Registry for mapping model names to their Docker configurations.
8
+
9
+ Thread-safe registry that maps model names to their Docker configuration factories.
10
+ """
11
+
12
+ def __init__(self):
13
+ import threading
14
+
15
+ self._registry: dict[str, Callable[[], BaseServerConfig | None]] = {}
16
+ self._lock = threading.RLock()
17
+
18
+ def register(
19
+ self,
20
+ model_name: str,
21
+ config_factory: Callable[[], BaseServerConfig | None],
22
+ ):
23
+ """Register a config factory for a model name (thread-safe)."""
24
+ with self._lock:
25
+ self._registry[model_name] = config_factory
26
+
27
+ def get(self, model_name: str) -> BaseServerConfig | None:
28
+ """Get config for a model name (thread-safe). Returns None if not registered."""
29
+ with self._lock:
30
+ if model_name not in self._registry:
31
+ return None
32
+ factory = self._registry[model_name]
33
+ return factory()
34
+
35
+ def list_models(self) -> list[str]:
36
+ """List all registered model names (thread-safe)."""
37
+ with self._lock:
38
+ return list(self._registry.keys())
39
+
40
+
41
+ # Global registry instance
42
+ docker_config_registry = DockerConfigRegistry()