vlmparse 0.1.8__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.
- vlmparse/cli.py +439 -286
- vlmparse/clients/deepseekocr.py +170 -0
- vlmparse/clients/glmocr.py +243 -0
- vlmparse/clients/paddleocrvl.py +191 -43
- vlmparse/converter_with_server.py +53 -16
- vlmparse/registries.py +20 -10
- vlmparse/servers/base_server.py +127 -0
- vlmparse/servers/docker_compose_deployment.py +489 -0
- vlmparse/servers/docker_compose_server.py +39 -0
- vlmparse/servers/docker_run_deployment.py +226 -0
- vlmparse/servers/docker_server.py +9 -125
- vlmparse/servers/server_registry.py +42 -0
- vlmparse/servers/utils.py +83 -219
- vlmparse/st_viewer/st_viewer.py +1 -1
- {vlmparse-0.1.8.dist-info → vlmparse-0.1.9.dist-info}/METADATA +3 -3
- {vlmparse-0.1.8.dist-info → vlmparse-0.1.9.dist-info}/RECORD +20 -14
- {vlmparse-0.1.8.dist-info → vlmparse-0.1.9.dist-info}/WHEEL +0 -0
- {vlmparse-0.1.8.dist-info → vlmparse-0.1.9.dist-info}/entry_points.txt +0 -0
- {vlmparse-0.1.8.dist-info → vlmparse-0.1.9.dist-info}/licenses/LICENSE +0 -0
- {vlmparse-0.1.8.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,52 +1,28 @@
|
|
|
1
1
|
import os
|
|
2
|
-
from typing import Callable
|
|
3
2
|
|
|
4
|
-
from loguru import logger
|
|
5
3
|
from pydantic import Field
|
|
6
4
|
|
|
7
|
-
from .
|
|
8
|
-
from .
|
|
5
|
+
from .base_server import BaseServer, BaseServerConfig
|
|
6
|
+
from .docker_run_deployment import docker_server
|
|
9
7
|
|
|
10
8
|
|
|
11
|
-
class DockerServerConfig(
|
|
12
|
-
"""
|
|
9
|
+
class DockerServerConfig(BaseServerConfig):
|
|
10
|
+
"""Configuration for deploying a Docker server.
|
|
13
11
|
|
|
14
|
-
Inherits from
|
|
15
|
-
- model_name: str
|
|
16
|
-
- default_model_name: str | None
|
|
17
|
-
- aliases: list[str]
|
|
18
|
-
- _create_client_kwargs(base_url): Helper for creating client configs
|
|
19
|
-
- get_all_names(): All names this model can be referenced by
|
|
12
|
+
Inherits from BaseServerConfig which provides common server configuration.
|
|
20
13
|
"""
|
|
21
14
|
|
|
22
15
|
docker_image: str
|
|
23
16
|
dockerfile_dir: str | None = None
|
|
24
17
|
command_args: list[str] = Field(default_factory=list)
|
|
25
|
-
server_ready_indicators: list[str] = Field(
|
|
26
|
-
default_factory=lambda: [
|
|
27
|
-
"Application startup complete",
|
|
28
|
-
"Uvicorn running",
|
|
29
|
-
"Starting vLLM API server",
|
|
30
|
-
]
|
|
31
|
-
)
|
|
32
|
-
docker_port: int = 8056
|
|
33
|
-
gpu_device_ids: list[str] | None = None
|
|
34
|
-
container_port: int = 8000
|
|
35
|
-
environment: dict[str, str] = Field(default_factory=dict)
|
|
36
18
|
volumes: dict[str, dict] | None = None
|
|
37
19
|
entrypoint: str | None = None
|
|
38
20
|
|
|
39
|
-
class Config:
|
|
40
|
-
extra = "allow"
|
|
41
|
-
|
|
42
21
|
@property
|
|
43
22
|
def client_config(self):
|
|
44
23
|
"""Override in subclasses to return appropriate client config."""
|
|
45
24
|
raise NotImplementedError
|
|
46
25
|
|
|
47
|
-
def get_client(self, **kwargs):
|
|
48
|
-
return self.client_config.get_client(**kwargs)
|
|
49
|
-
|
|
50
26
|
def get_server(self, auto_stop: bool = True):
|
|
51
27
|
return ConverterServer(config=self, auto_stop=auto_stop)
|
|
52
28
|
|
|
@@ -71,14 +47,6 @@ class DockerServerConfig(ModelIdentityMixin):
|
|
|
71
47
|
"""Setup volumes for container. Override in subclasses for specific logic."""
|
|
72
48
|
return self.volumes
|
|
73
49
|
|
|
74
|
-
def get_environment(self) -> dict | None:
|
|
75
|
-
"""Setup environment variables. Override in subclasses for specific logic."""
|
|
76
|
-
return self.environment if self.environment else None
|
|
77
|
-
|
|
78
|
-
def get_base_url_suffix(self) -> str:
|
|
79
|
-
"""Return URL suffix (e.g., '/v1' for OpenAI-compatible APIs). Override in subclasses."""
|
|
80
|
-
return ""
|
|
81
|
-
|
|
82
50
|
|
|
83
51
|
DEFAULT_MODEL_NAME = "vllm-model"
|
|
84
52
|
|
|
@@ -144,93 +112,9 @@ class VLLMDockerServerConfig(DockerServerConfig):
|
|
|
144
112
|
return "/v1"
|
|
145
113
|
|
|
146
114
|
|
|
147
|
-
class ConverterServer:
|
|
115
|
+
class ConverterServer(BaseServer):
|
|
148
116
|
"""Manages Docker server lifecycle with start/stop methods."""
|
|
149
117
|
|
|
150
|
-
def
|
|
151
|
-
|
|
152
|
-
self.
|
|
153
|
-
self._server_context = None
|
|
154
|
-
self._container = None
|
|
155
|
-
self.base_url = None
|
|
156
|
-
|
|
157
|
-
def start(self):
|
|
158
|
-
"""Start the Docker server."""
|
|
159
|
-
if self._server_context is not None:
|
|
160
|
-
logger.warning("Server already started")
|
|
161
|
-
return self.base_url, self._container
|
|
162
|
-
|
|
163
|
-
# Use the generic docker_server for all server types
|
|
164
|
-
self._server_context = docker_server(config=self.config, cleanup=self.auto_stop)
|
|
165
|
-
|
|
166
|
-
self.base_url, self._container = self._server_context.__enter__()
|
|
167
|
-
logger.info(f"Server started at {self.base_url}")
|
|
168
|
-
logger.info(f"Container ID: {self._container.id}")
|
|
169
|
-
logger.info(f"Container name: {self._container.name}")
|
|
170
|
-
return self.base_url, self._container
|
|
171
|
-
|
|
172
|
-
def stop(self):
|
|
173
|
-
"""Stop the Docker server."""
|
|
174
|
-
if self._server_context is not None:
|
|
175
|
-
try:
|
|
176
|
-
self._server_context.__exit__(None, None, None)
|
|
177
|
-
except Exception as e:
|
|
178
|
-
logger.warning(f"Error during server cleanup: {e}")
|
|
179
|
-
finally:
|
|
180
|
-
self._server_context = None
|
|
181
|
-
self._container = None
|
|
182
|
-
self.base_url = None
|
|
183
|
-
logger.info("Server stopped")
|
|
184
|
-
|
|
185
|
-
def __del__(self):
|
|
186
|
-
"""Automatically stop server when object is destroyed if auto_stop is True.
|
|
187
|
-
|
|
188
|
-
Note: This is a fallback mechanism. Prefer using the context manager
|
|
189
|
-
or explicitly calling stop() for reliable cleanup.
|
|
190
|
-
"""
|
|
191
|
-
try:
|
|
192
|
-
if self.auto_stop and self._server_context is not None:
|
|
193
|
-
self.stop()
|
|
194
|
-
except Exception:
|
|
195
|
-
pass # Suppress errors during garbage collection
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
class DockerConfigRegistry:
|
|
199
|
-
"""Registry for mapping model names to their Docker configurations.
|
|
200
|
-
|
|
201
|
-
Thread-safe registry that maps model names to their Docker configuration factories.
|
|
202
|
-
"""
|
|
203
|
-
|
|
204
|
-
def __init__(self):
|
|
205
|
-
import threading
|
|
206
|
-
|
|
207
|
-
self._registry: dict[str, Callable[[], DockerServerConfig | None]] = {}
|
|
208
|
-
self._lock = threading.RLock()
|
|
209
|
-
|
|
210
|
-
def register(
|
|
211
|
-
self, model_name: str, config_factory: Callable[[], DockerServerConfig | None]
|
|
212
|
-
):
|
|
213
|
-
"""Register a config factory for a model name (thread-safe)."""
|
|
214
|
-
with self._lock:
|
|
215
|
-
self._registry[model_name] = config_factory
|
|
216
|
-
|
|
217
|
-
def get(self, model_name: str, default=False) -> DockerServerConfig | None:
|
|
218
|
-
"""Get config for a model name (thread-safe). Returns default if not registered."""
|
|
219
|
-
with self._lock:
|
|
220
|
-
if model_name not in self._registry:
|
|
221
|
-
if default:
|
|
222
|
-
return VLLMDockerServerConfig(
|
|
223
|
-
model_name=model_name, default_model_name=DEFAULT_MODEL_NAME
|
|
224
|
-
)
|
|
225
|
-
return None
|
|
226
|
-
factory = self._registry[model_name]
|
|
227
|
-
return factory()
|
|
228
|
-
|
|
229
|
-
def list_models(self) -> list[str]:
|
|
230
|
-
"""List all registered model names (thread-safe)."""
|
|
231
|
-
with self._lock:
|
|
232
|
-
return list(self._registry.keys())
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
# Global registry instance
|
|
236
|
-
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,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()
|