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/cli.py +438 -286
- vlmparse/clients/deepseekocr.py +170 -0
- vlmparse/clients/glmocr.py +243 -0
- vlmparse/clients/paddleocrvl.py +191 -43
- vlmparse/converter_with_server.py +57 -20
- 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.10.dist-info}/METADATA +7 -7
- {vlmparse-0.1.8.dist-info → vlmparse-0.1.10.dist-info}/RECORD +20 -14
- {vlmparse-0.1.8.dist-info → vlmparse-0.1.10.dist-info}/WHEEL +0 -0
- {vlmparse-0.1.8.dist-info → vlmparse-0.1.10.dist-info}/entry_points.txt +0 -0
- {vlmparse-0.1.8.dist-info → vlmparse-0.1.10.dist-info}/licenses/LICENSE +0 -0
- {vlmparse-0.1.8.dist-info → vlmparse-0.1.10.dist-info}/top_level.txt +0 -0
vlmparse/cli.py
CHANGED
|
@@ -1,334 +1,486 @@
|
|
|
1
|
+
# ruff: noqa: B008
|
|
1
2
|
from typing import Literal
|
|
2
3
|
|
|
4
|
+
import typer
|
|
3
5
|
from loguru import logger
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
7
|
+
app = typer.Typer(help="Parse PDF documents with VLMs.", pretty_exceptions_enable=False)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@app.command("serve")
|
|
11
|
+
def serve(
|
|
12
|
+
model: str = typer.Argument(..., help="Model name"),
|
|
13
|
+
port: int | None = typer.Option(None, help="VLLM server port (default: 8056)"),
|
|
14
|
+
gpus: str | None = typer.Option(
|
|
15
|
+
None,
|
|
16
|
+
help='Comma-separated GPU device IDs (e.g., "0" or "0,1,2"). If not specified, all GPUs will be used.',
|
|
17
|
+
),
|
|
18
|
+
provider: Literal["registry", "hf"] = typer.Option(
|
|
19
|
+
"registry", help="provider type for the model. 'registry' (default) or 'hf'."
|
|
20
|
+
),
|
|
21
|
+
vllm_args: list[str] | None = typer.Option(
|
|
22
|
+
None,
|
|
23
|
+
"--vllm-args",
|
|
24
|
+
help="Additional keyword arguments to pass to the VLLM server.",
|
|
25
|
+
),
|
|
26
|
+
forget_predefined_vllm_args: bool = typer.Option(
|
|
27
|
+
False,
|
|
28
|
+
help=(
|
|
29
|
+
"If True, the predefined VLLM kwargs from the docker config will be replaced by vllm_args. "
|
|
30
|
+
"Otherwise they will be updated with vllm_args (may overwrite keys)."
|
|
31
|
+
),
|
|
32
|
+
),
|
|
33
|
+
):
|
|
34
|
+
"""Deploy a VLLM server in a Docker container.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
model: Model name
|
|
38
|
+
port: VLLM server port (default: 8056)
|
|
39
|
+
gpus: Comma-separated GPU device IDs (e.g., "0" or "0,1,2"). If not specified, GPU 0 will be used.
|
|
40
|
+
server: Server type for the model. 'registry' (default) or 'hf'.
|
|
41
|
+
vllm_args: Additional keyword arguments to pass to the VLLM server.
|
|
42
|
+
forget_predefined_vllm_args: If True, the predefined VLLM kwargs from the docker config will be replaced by vllm_args otherwise the predefined kwargs will be updated with vllm_args with a risk of collision of argument names.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
from vlmparse.converter_with_server import start_server
|
|
46
|
+
|
|
47
|
+
base_url, container, _, _ = start_server(
|
|
48
|
+
model=model,
|
|
49
|
+
gpus=gpus,
|
|
50
|
+
port=port,
|
|
51
|
+
provider=provider,
|
|
52
|
+
vllm_args=vllm_args,
|
|
53
|
+
forget_predefined_vllm_args=forget_predefined_vllm_args,
|
|
54
|
+
auto_stop=False,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
logger.info(f"✓ VLLM server ready at {base_url}")
|
|
58
|
+
if container is not None:
|
|
59
|
+
logger.info(f"✓ Container ID: {container.id}")
|
|
60
|
+
logger.info(f"✓ Container name: {container.name}")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@app.command("convert")
|
|
64
|
+
def convert(
|
|
65
|
+
inputs: str = typer.Argument(..., help="List of folders to process"),
|
|
66
|
+
out_folder: str = typer.Option(".", help="Output folder for parsed documents"),
|
|
67
|
+
model: str | None = typer.Option(
|
|
68
|
+
None, help="Model name. If not specified, inferred from the URI."
|
|
69
|
+
),
|
|
70
|
+
uri: str | None = typer.Option(
|
|
71
|
+
None,
|
|
72
|
+
help=(
|
|
73
|
+
"URI of the server. If not specified and the pipe is vllm, "
|
|
74
|
+
"a local server will be deployed."
|
|
75
|
+
),
|
|
76
|
+
),
|
|
77
|
+
gpus: str | None = typer.Option(
|
|
78
|
+
None,
|
|
79
|
+
help='Comma-separated GPU device IDs (e.g., "0" or "0,1,2"). If not specified, GPU 0 will be used.',
|
|
80
|
+
),
|
|
81
|
+
mode: Literal["document", "md", "md_page"] = typer.Option(
|
|
82
|
+
"document",
|
|
83
|
+
help=(
|
|
84
|
+
"Output mode - document (save as JSON zip), md (save as markdown file), "
|
|
85
|
+
"md_page (save as folder of markdown pages)"
|
|
86
|
+
),
|
|
87
|
+
),
|
|
88
|
+
conversion_mode: Literal[
|
|
89
|
+
"ocr",
|
|
90
|
+
"ocr_layout",
|
|
91
|
+
"table",
|
|
92
|
+
"image_description",
|
|
93
|
+
"formula",
|
|
94
|
+
"chart",
|
|
95
|
+
] = typer.Option(
|
|
96
|
+
"ocr",
|
|
97
|
+
help=(
|
|
98
|
+
"Conversion mode - ocr (plain), ocr_layout (OCR with layout), table (table-centric), "
|
|
99
|
+
"image_description (describe the image), formula (formula extraction), chart (chart recognition)"
|
|
100
|
+
),
|
|
101
|
+
),
|
|
102
|
+
provider: Literal["registry", "hf", "google", "openai"] = typer.Option(
|
|
103
|
+
"registry",
|
|
104
|
+
help="Server type for the model. Defaults to 'registry'.",
|
|
105
|
+
),
|
|
106
|
+
with_vllm_server: bool = typer.Option(
|
|
107
|
+
False,
|
|
108
|
+
help=(
|
|
109
|
+
"Deprecated. Use --server hf instead. "
|
|
110
|
+
"If True, a local VLLM server will be deployed if the model is not found in the registry. "
|
|
111
|
+
"Note that if the model is in the registry and uri is None, the server will be deployed."
|
|
112
|
+
),
|
|
113
|
+
),
|
|
114
|
+
concurrency: int = typer.Option(10, help="Number of parallel requests"),
|
|
115
|
+
dpi: int | None = typer.Option(None, help="DPI to use for the conversion"),
|
|
116
|
+
debug: bool = typer.Option(False, help="Run in debug mode"),
|
|
117
|
+
_return_documents: bool = typer.Option(False, hidden=True),
|
|
118
|
+
):
|
|
119
|
+
"""Parse PDF documents and save results.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
inputs: List of folders to process
|
|
123
|
+
out_folder: Output folder for parsed documents
|
|
124
|
+
pipe: Converter type ("vllm", "openai", or "lightonocr", default: "vllm")
|
|
125
|
+
model: Model name. If not specified, the model will be inferred from the URI.
|
|
126
|
+
uri: URI of the server, if not specified and the pipe is vllm, a local server will be deployed
|
|
127
|
+
gpus: Comma-separated GPU device IDs (e.g., "0" or "0,1,2"). If not specified, all GPUs will be used.
|
|
128
|
+
mode: Output mode - "document" (save as JSON zip), "md" (save as markdown file), "md_page" (save as folder of markdown pages)
|
|
129
|
+
conversion_mode: Conversion mode - "ocr" (plain), "ocr_layout" (OCR with layout), "table" (table-centric), "image_description" (describe the image), "formula" (formula extraction), "chart" (chart recognition)
|
|
130
|
+
provider: provider type for the model. Defaults to 'registry'.
|
|
131
|
+
dpi: DPI to use for the conversion. If not specified, the default DPI will be used.
|
|
132
|
+
debug: If True, run in debug mode (single-threaded, no concurrency)
|
|
133
|
+
"""
|
|
134
|
+
from vlmparse.converter_with_server import ConverterWithServer
|
|
135
|
+
|
|
136
|
+
if with_vllm_server and provider == "registry":
|
|
137
|
+
provider = "hf"
|
|
138
|
+
|
|
139
|
+
with ConverterWithServer(
|
|
140
|
+
model=model,
|
|
141
|
+
uri=uri,
|
|
142
|
+
gpus=gpus,
|
|
143
|
+
provider=provider,
|
|
144
|
+
concurrency=concurrency,
|
|
145
|
+
return_documents=_return_documents,
|
|
146
|
+
) as converter_with_server:
|
|
147
|
+
return converter_with_server.parse(
|
|
148
|
+
inputs=inputs,
|
|
149
|
+
out_folder=out_folder,
|
|
150
|
+
mode=mode,
|
|
151
|
+
conversion_mode=conversion_mode,
|
|
152
|
+
dpi=dpi,
|
|
153
|
+
debug=debug,
|
|
37
154
|
)
|
|
38
155
|
|
|
39
|
-
logger.info(f"✓ VLLM server ready at {base_url}")
|
|
40
|
-
if container is not None:
|
|
41
|
-
logger.info(f"✓ Container ID: {container.id}")
|
|
42
|
-
logger.info(f"✓ Container name: {container.name}")
|
|
43
|
-
|
|
44
|
-
def convert(
|
|
45
|
-
self,
|
|
46
|
-
inputs: str | list[str],
|
|
47
|
-
out_folder: str = ".",
|
|
48
|
-
model: str | None = None,
|
|
49
|
-
uri: str | None = None,
|
|
50
|
-
gpus: str | None = None,
|
|
51
|
-
mode: Literal["document", "md", "md_page"] = "document",
|
|
52
|
-
conversion_mode: Literal[
|
|
53
|
-
"ocr",
|
|
54
|
-
"ocr_layout",
|
|
55
|
-
"table",
|
|
56
|
-
"image_description",
|
|
57
|
-
"formula",
|
|
58
|
-
"chart",
|
|
59
|
-
] = "ocr",
|
|
60
|
-
with_vllm_server: bool = False,
|
|
61
|
-
concurrency: int = 10,
|
|
62
|
-
dpi: int | None = None,
|
|
63
|
-
debug: bool = False,
|
|
64
|
-
_return_documents: bool = False,
|
|
65
|
-
):
|
|
66
|
-
"""Parse PDF documents and save results.
|
|
67
|
-
|
|
68
|
-
Args:
|
|
69
|
-
inputs: List of folders to process
|
|
70
|
-
out_folder: Output folder for parsed documents
|
|
71
|
-
pipe: Converter type ("vllm", "openai", or "lightonocr", default: "vllm")
|
|
72
|
-
model: Model name. If not specified, the model will be inferred from the URI.
|
|
73
|
-
uri: URI of the server, if not specified and the pipe is vllm, a local server will be deployed
|
|
74
|
-
gpus: Comma-separated GPU device IDs (e.g., "0" or "0,1,2"). If not specified, all GPUs will be used.
|
|
75
|
-
mode: Output mode - "document" (save as JSON zip), "md" (save as markdown file), "md_page" (save as folder of markdown pages)
|
|
76
|
-
conversion_mode: Conversion mode - "ocr" (plain), "ocr_layout" (OCR with layout), "table" (table-centric), "image_description" (describe the image), "formula" (formula extraction), "chart" (chart recognition)
|
|
77
|
-
with_vllm_server: If True, a local VLLM server will be deployed if the model is not found in the registry. Note that if the model is in the registry and the uri is None, the server will be anyway deployed.
|
|
78
|
-
dpi: DPI to use for the conversion. If not specified, the default DPI will be used.
|
|
79
|
-
debug: If True, run in debug mode (single-threaded, no concurrency)
|
|
80
|
-
"""
|
|
81
|
-
from vlmparse.converter_with_server import ConverterWithServer
|
|
82
|
-
|
|
83
|
-
with ConverterWithServer(
|
|
84
|
-
model=model,
|
|
85
|
-
uri=uri,
|
|
86
|
-
gpus=gpus,
|
|
87
|
-
with_vllm_server=with_vllm_server,
|
|
88
|
-
concurrency=concurrency,
|
|
89
|
-
return_documents=_return_documents,
|
|
90
|
-
) as converter_with_server:
|
|
91
|
-
return converter_with_server.parse(
|
|
92
|
-
inputs=inputs,
|
|
93
|
-
out_folder=out_folder,
|
|
94
|
-
mode=mode,
|
|
95
|
-
conversion_mode=conversion_mode,
|
|
96
|
-
dpi=dpi,
|
|
97
|
-
debug=debug,
|
|
98
|
-
)
|
|
99
156
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
157
|
+
@app.command("list")
|
|
158
|
+
def containers():
|
|
159
|
+
"""List all containers whose name begins with vlmparse."""
|
|
160
|
+
import docker
|
|
103
161
|
|
|
104
|
-
|
|
105
|
-
client = docker.from_env()
|
|
106
|
-
containers = client.containers.list()
|
|
162
|
+
from vlmparse.servers.utils import _get_container_labels
|
|
107
163
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
164
|
+
try:
|
|
165
|
+
client = docker.from_env()
|
|
166
|
+
all_containers = client.containers.list(all=True)
|
|
111
167
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
for container in containers
|
|
116
|
-
if container.name.startswith("vlmparse")
|
|
117
|
-
]
|
|
168
|
+
if not all_containers:
|
|
169
|
+
logger.info("No containers found")
|
|
170
|
+
return
|
|
118
171
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
return
|
|
172
|
+
# Group containers by compose project or as standalone
|
|
173
|
+
projects = {} # project_name -> list of containers
|
|
122
174
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
175
|
+
for container in all_containers:
|
|
176
|
+
labels = _get_container_labels(container)
|
|
177
|
+
project = labels.get("com.docker.compose.project") or labels.get(
|
|
178
|
+
"vlmparse_compose_project"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# Include if name starts with vlmparse OR if it's in a vlmparse compose project
|
|
182
|
+
if container.name.startswith("vlmparse"):
|
|
183
|
+
if project:
|
|
184
|
+
projects.setdefault(project, []).append(container)
|
|
185
|
+
else:
|
|
186
|
+
projects[container.name] = [container]
|
|
187
|
+
elif project and project.startswith("vlmparse"):
|
|
188
|
+
projects.setdefault(project, []).append(container)
|
|
189
|
+
|
|
190
|
+
if not projects:
|
|
191
|
+
logger.info("No vlmparse containers found")
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
# Prepare table data - one row per project/standalone container
|
|
195
|
+
table_data = []
|
|
196
|
+
for project_name, project_containers in projects.items():
|
|
197
|
+
# Find the main container with vlmparse labels
|
|
198
|
+
main_container = project_containers[0]
|
|
199
|
+
for c in project_containers:
|
|
200
|
+
labels = _get_container_labels(c)
|
|
201
|
+
if labels.get("vlmparse_uri"):
|
|
202
|
+
main_container = c
|
|
203
|
+
break
|
|
204
|
+
|
|
205
|
+
labels = _get_container_labels(main_container)
|
|
206
|
+
|
|
207
|
+
# Extract port mappings from all containers in the project
|
|
208
|
+
ports = []
|
|
209
|
+
for container in project_containers:
|
|
128
210
|
if container.ports:
|
|
129
211
|
for _, host_bindings in container.ports.items():
|
|
130
212
|
if host_bindings:
|
|
131
213
|
for binding in host_bindings:
|
|
132
214
|
ports.append(f"{binding['HostPort']}")
|
|
133
215
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
table_data.append(
|
|
139
|
-
[
|
|
140
|
-
container.name,
|
|
141
|
-
container.status,
|
|
142
|
-
port_str,
|
|
143
|
-
gpu,
|
|
144
|
-
uri,
|
|
145
|
-
]
|
|
146
|
-
)
|
|
216
|
+
port_str = ", ".join(sorted(set(ports))) if ports else "N/A"
|
|
217
|
+
uri = labels.get("vlmparse_uri", "N/A")
|
|
218
|
+
gpu = labels.get("vlmparse_gpus", "N/A")
|
|
147
219
|
|
|
148
|
-
#
|
|
149
|
-
|
|
220
|
+
# Get all statuses
|
|
221
|
+
statuses = list(set(c.status for c in project_containers))
|
|
222
|
+
status_str = (
|
|
223
|
+
statuses[0] if len(statuses) == 1 else f"mixed ({', '.join(statuses)})"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
# Name: show project name if compose, otherwise container name
|
|
227
|
+
is_compose = labels.get("com.docker.compose.project") or labels.get(
|
|
228
|
+
"vlmparse_compose_project"
|
|
229
|
+
)
|
|
230
|
+
name = project_name if is_compose else main_container.name
|
|
150
231
|
|
|
151
|
-
|
|
152
|
-
table = tabulate(table_data, headers=headers, tablefmt="grid")
|
|
232
|
+
table_data.append([name, status_str, port_str, gpu, uri])
|
|
153
233
|
|
|
154
|
-
|
|
155
|
-
|
|
234
|
+
# Display as table
|
|
235
|
+
from tabulate import tabulate
|
|
156
236
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
logger.error(
|
|
160
|
-
"Make sure Docker is running and you have the necessary permissions"
|
|
161
|
-
)
|
|
237
|
+
headers = ["Name", "Status", "Port(s)", "GPU", "URI"]
|
|
238
|
+
table = tabulate(table_data, headers=headers, tablefmt="grid")
|
|
162
239
|
|
|
163
|
-
|
|
164
|
-
|
|
240
|
+
total = sum(len(containers) for containers in projects.values())
|
|
241
|
+
logger.info(
|
|
242
|
+
f"\nFound {len(projects)} vlmparse deployment(s) ({total} container(s)):\n"
|
|
243
|
+
)
|
|
244
|
+
print(table)
|
|
165
245
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
246
|
+
except docker.errors.DockerException as e:
|
|
247
|
+
logger.error(f"Failed to connect to Docker: {e}")
|
|
248
|
+
logger.error(
|
|
249
|
+
"Make sure Docker is running and you have the necessary permissions"
|
|
250
|
+
)
|
|
170
251
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
252
|
+
|
|
253
|
+
@app.command("stop")
|
|
254
|
+
def stop(
|
|
255
|
+
container: str | None = typer.Argument(None, help="Container ID or name to stop"),
|
|
256
|
+
):
|
|
257
|
+
"""Stop a Docker container by its ID or name.
|
|
258
|
+
|
|
259
|
+
If the selected container belongs to a Docker Compose project, the whole
|
|
260
|
+
compose stack is stopped and removed.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
container: Container ID or name to stop. If not specified, automatically stops the container if only one vlmparse container is running.
|
|
264
|
+
"""
|
|
265
|
+
import docker
|
|
266
|
+
|
|
267
|
+
from vlmparse.servers.utils import _stop_compose_stack_for_container
|
|
268
|
+
|
|
269
|
+
try:
|
|
270
|
+
client = docker.from_env()
|
|
271
|
+
|
|
272
|
+
# If no container specified, try to auto-select
|
|
273
|
+
if container is None:
|
|
274
|
+
from vlmparse.servers.utils import _get_container_labels
|
|
275
|
+
|
|
276
|
+
all_containers = client.containers.list()
|
|
277
|
+
|
|
278
|
+
# Group containers by compose project or as standalone
|
|
279
|
+
projects = {} # project_name -> list of containers
|
|
280
|
+
for c in all_containers:
|
|
281
|
+
labels = _get_container_labels(c)
|
|
282
|
+
project = labels.get("com.docker.compose.project") or labels.get(
|
|
283
|
+
"vlmparse_compose_project"
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# Include if name starts with vlmparse OR if it's in a vlmparse compose project
|
|
287
|
+
if c.name.startswith("vlmparse"):
|
|
288
|
+
if project:
|
|
289
|
+
projects.setdefault(project, []).append(c)
|
|
290
|
+
else:
|
|
291
|
+
projects[c.name] = [c]
|
|
292
|
+
elif project and project.startswith("vlmparse"):
|
|
293
|
+
projects.setdefault(project, []).append(c)
|
|
294
|
+
|
|
295
|
+
if len(projects) == 0:
|
|
296
|
+
logger.error("No vlmparse containers found")
|
|
297
|
+
return
|
|
298
|
+
elif len(projects) > 1:
|
|
299
|
+
logger.error(
|
|
300
|
+
f"Multiple vlmparse deployments found ({len(projects)}). "
|
|
301
|
+
"Please specify a container ID or name:"
|
|
302
|
+
)
|
|
303
|
+
for project_name, containers in projects.items():
|
|
304
|
+
if len(containers) > 1:
|
|
305
|
+
logger.info(
|
|
306
|
+
f" - {project_name} ({len(containers)} containers)"
|
|
307
|
+
)
|
|
308
|
+
else:
|
|
309
|
+
logger.info(
|
|
310
|
+
f" - {containers[0].name} ({containers[0].short_id})"
|
|
311
|
+
)
|
|
312
|
+
return
|
|
194
313
|
else:
|
|
195
|
-
#
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
314
|
+
# Only one project/deployment, pick any container from it
|
|
315
|
+
target_container = list(projects.values())[0][0]
|
|
316
|
+
else:
|
|
317
|
+
# Try to get the specified container
|
|
318
|
+
try:
|
|
319
|
+
target_container = client.containers.get(container)
|
|
320
|
+
except docker.errors.NotFound:
|
|
321
|
+
logger.error(f"Container not found: {container}")
|
|
322
|
+
return
|
|
323
|
+
|
|
324
|
+
# If the container is part of a docker-compose stack, bring the whole stack down.
|
|
325
|
+
if _stop_compose_stack_for_container(target_container):
|
|
326
|
+
return
|
|
327
|
+
|
|
328
|
+
# Stop + remove the container
|
|
329
|
+
logger.info(
|
|
330
|
+
f"Stopping container: {target_container.name} ({target_container.short_id})"
|
|
331
|
+
)
|
|
332
|
+
try:
|
|
206
333
|
target_container.stop()
|
|
207
|
-
|
|
334
|
+
except Exception:
|
|
335
|
+
pass
|
|
336
|
+
try:
|
|
337
|
+
target_container.remove(force=True)
|
|
338
|
+
except Exception:
|
|
339
|
+
pass
|
|
340
|
+
logger.info("✓ Container stopped and removed successfully")
|
|
341
|
+
|
|
342
|
+
except docker.errors.DockerException as e:
|
|
343
|
+
logger.error(f"Failed to connect to Docker: {e}")
|
|
344
|
+
logger.error(
|
|
345
|
+
"Make sure Docker is running and you have the necessary permissions"
|
|
346
|
+
)
|
|
208
347
|
|
|
209
|
-
except docker.errors.DockerException as e:
|
|
210
|
-
logger.error(f"Failed to connect to Docker: {e}")
|
|
211
|
-
logger.error(
|
|
212
|
-
"Make sure Docker is running and you have the necessary permissions"
|
|
213
|
-
)
|
|
214
348
|
|
|
215
|
-
|
|
216
|
-
|
|
349
|
+
@app.command("log")
|
|
350
|
+
def log(
|
|
351
|
+
container: str | None = typer.Argument(
|
|
352
|
+
None, help="Container ID or name. If not specified, auto-selects."
|
|
353
|
+
),
|
|
354
|
+
follow: bool = typer.Option(True, help="Follow log output"),
|
|
355
|
+
tail: int = typer.Option(500, help="Number of lines to show from the end"),
|
|
356
|
+
):
|
|
357
|
+
"""Show logs from a Docker container.
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
container: Container ID or name. If not specified, automatically selects the container if only one vlmparse container is running.
|
|
361
|
+
follow: If True, follow log output (stream logs in real-time)
|
|
362
|
+
tail: Number of lines to show from the end of the logs
|
|
363
|
+
"""
|
|
364
|
+
import docker
|
|
365
|
+
|
|
366
|
+
try:
|
|
367
|
+
client = docker.from_env()
|
|
368
|
+
|
|
369
|
+
# If no container specified, try to auto-select
|
|
370
|
+
if container is None:
|
|
371
|
+
from vlmparse.servers.utils import _get_container_labels
|
|
372
|
+
|
|
373
|
+
all_containers = client.containers.list()
|
|
374
|
+
vlmparse_containers = []
|
|
375
|
+
|
|
376
|
+
for c in all_containers:
|
|
377
|
+
labels = _get_container_labels(c)
|
|
378
|
+
project = labels.get("com.docker.compose.project") or labels.get(
|
|
379
|
+
"vlmparse_compose_project"
|
|
380
|
+
)
|
|
217
381
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
import docker
|
|
382
|
+
# Include if name starts with vlmparse OR if it's in a vlmparse compose project
|
|
383
|
+
if c.name.startswith("vlmparse") or (
|
|
384
|
+
project and project.startswith("vlmparse")
|
|
385
|
+
):
|
|
386
|
+
vlmparse_containers.append(c)
|
|
224
387
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
logger.error("No vlmparse containers found")
|
|
237
|
-
return
|
|
238
|
-
elif len(vlmparse_containers) > 1:
|
|
239
|
-
logger.error(
|
|
240
|
-
f"Multiple vlmparse containers found ({len(vlmparse_containers)}). "
|
|
241
|
-
"Please specify a container ID or name:"
|
|
242
|
-
)
|
|
243
|
-
for c in vlmparse_containers:
|
|
244
|
-
logger.info(f" - {c.name} ({c.short_id})")
|
|
245
|
-
return
|
|
246
|
-
else:
|
|
247
|
-
target_container = vlmparse_containers[0]
|
|
248
|
-
logger.info(
|
|
249
|
-
f"Showing logs for: {target_container.name} ({target_container.short_id})"
|
|
250
|
-
)
|
|
251
|
-
else:
|
|
252
|
-
# Try to get the specified container
|
|
253
|
-
try:
|
|
254
|
-
target_container = client.containers.get(container)
|
|
255
|
-
except docker.errors.NotFound:
|
|
256
|
-
logger.error(f"Container not found: {container}")
|
|
257
|
-
return
|
|
258
|
-
|
|
259
|
-
# Get and display logs
|
|
260
|
-
if follow:
|
|
261
|
-
logger.info("Following logs (press Ctrl+C to stop)...")
|
|
262
|
-
try:
|
|
263
|
-
for log_line in target_container.logs(
|
|
264
|
-
stream=True, follow=True, tail=tail
|
|
265
|
-
):
|
|
266
|
-
print(log_line.decode("utf-8", errors="replace"), end="")
|
|
267
|
-
except KeyboardInterrupt:
|
|
268
|
-
logger.info("\nStopped following logs")
|
|
388
|
+
if len(vlmparse_containers) == 0:
|
|
389
|
+
logger.error("No vlmparse containers found")
|
|
390
|
+
return
|
|
391
|
+
elif len(vlmparse_containers) > 1:
|
|
392
|
+
logger.error(
|
|
393
|
+
f"Multiple vlmparse containers found ({len(vlmparse_containers)}). "
|
|
394
|
+
"Please specify a container ID or name:"
|
|
395
|
+
)
|
|
396
|
+
for c in vlmparse_containers:
|
|
397
|
+
logger.info(f" - {c.name} ({c.short_id})")
|
|
398
|
+
return
|
|
269
399
|
else:
|
|
270
|
-
|
|
271
|
-
|
|
400
|
+
target_container = vlmparse_containers[0]
|
|
401
|
+
logger.info(
|
|
402
|
+
f"Showing logs for: {target_container.name} ({target_container.short_id})"
|
|
403
|
+
)
|
|
404
|
+
else:
|
|
405
|
+
# Try to get the specified container
|
|
406
|
+
try:
|
|
407
|
+
target_container = client.containers.get(container)
|
|
408
|
+
except docker.errors.NotFound:
|
|
409
|
+
logger.error(f"Container not found: {container}")
|
|
410
|
+
return
|
|
272
411
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
logger.
|
|
276
|
-
|
|
277
|
-
|
|
412
|
+
# Get and display logs
|
|
413
|
+
if follow:
|
|
414
|
+
logger.info("Following logs (press Ctrl+C to stop)...")
|
|
415
|
+
try:
|
|
416
|
+
for log_line in target_container.logs(
|
|
417
|
+
stream=True, follow=True, tail=tail
|
|
418
|
+
):
|
|
419
|
+
print(log_line.decode("utf-8", errors="replace"), end="")
|
|
420
|
+
except KeyboardInterrupt:
|
|
421
|
+
logger.info("\nStopped following logs")
|
|
422
|
+
else:
|
|
423
|
+
logs = target_container.logs().decode("utf-8", errors="replace")
|
|
424
|
+
print(logs)
|
|
278
425
|
|
|
279
|
-
|
|
280
|
-
"
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
docker_config_registry,
|
|
426
|
+
except docker.errors.DockerException as e:
|
|
427
|
+
logger.error(f"Failed to connect to Docker: {e}")
|
|
428
|
+
logger.error(
|
|
429
|
+
"Make sure Docker is running and you have the necessary permissions"
|
|
284
430
|
)
|
|
285
431
|
|
|
286
|
-
client_models = sorted(converter_config_registry.list_models())
|
|
287
|
-
server_models = sorted(docker_config_registry.list_models())
|
|
288
432
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
433
|
+
@app.command("list-register")
|
|
434
|
+
def list_register():
|
|
435
|
+
"""List all model keys registered in client and server registries."""
|
|
436
|
+
from vlmparse.registries import converter_config_registry, docker_config_registry
|
|
292
437
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
print(f" - {model}")
|
|
438
|
+
client_models = sorted(converter_config_registry.list_models())
|
|
439
|
+
server_models = sorted(docker_config_registry.list_models())
|
|
296
440
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
441
|
+
print("\nClient Models Registry:")
|
|
442
|
+
for model in client_models:
|
|
443
|
+
print(f" - {model}")
|
|
300
444
|
|
|
301
|
-
|
|
445
|
+
print("\nServer Models Registry:")
|
|
446
|
+
for model in server_models:
|
|
447
|
+
print(f" - {model}")
|
|
302
448
|
|
|
303
|
-
from vlmparse.st_viewer.st_viewer import __file__ as st_viewer_file
|
|
304
|
-
from vlmparse.st_viewer.st_viewer import run_streamlit
|
|
305
449
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
subprocess.run(
|
|
311
|
-
[
|
|
312
|
-
sys.executable,
|
|
313
|
-
"-m",
|
|
314
|
-
"streamlit",
|
|
315
|
-
"run",
|
|
316
|
-
st_viewer_file,
|
|
317
|
-
"--",
|
|
318
|
-
folder,
|
|
319
|
-
],
|
|
320
|
-
check=True,
|
|
321
|
-
)
|
|
322
|
-
except KeyboardInterrupt:
|
|
323
|
-
print("\nStreamlit app terminated by user.")
|
|
324
|
-
except subprocess.CalledProcessError as e:
|
|
325
|
-
print(f"Error while running Streamlit: {e}")
|
|
450
|
+
@app.command("view")
|
|
451
|
+
def view(folder: str = typer.Argument(..., help="Folder to visualize with Streamlit")):
|
|
452
|
+
import subprocess
|
|
453
|
+
import sys
|
|
326
454
|
|
|
455
|
+
from streamlit import runtime
|
|
327
456
|
|
|
328
|
-
|
|
329
|
-
import
|
|
457
|
+
from vlmparse.st_viewer.st_viewer import __file__ as st_viewer_file
|
|
458
|
+
from vlmparse.st_viewer.st_viewer import run_streamlit
|
|
459
|
+
|
|
460
|
+
if runtime.exists():
|
|
461
|
+
run_streamlit(folder)
|
|
462
|
+
else:
|
|
463
|
+
try:
|
|
464
|
+
subprocess.run(
|
|
465
|
+
[
|
|
466
|
+
sys.executable,
|
|
467
|
+
"-m",
|
|
468
|
+
"streamlit",
|
|
469
|
+
"run",
|
|
470
|
+
st_viewer_file,
|
|
471
|
+
"--",
|
|
472
|
+
folder,
|
|
473
|
+
],
|
|
474
|
+
check=True,
|
|
475
|
+
)
|
|
476
|
+
except KeyboardInterrupt:
|
|
477
|
+
print("\nStreamlit app terminated by user.")
|
|
478
|
+
except subprocess.CalledProcessError as e:
|
|
479
|
+
print(f"Error while running Streamlit: {e}")
|
|
330
480
|
|
|
331
|
-
|
|
481
|
+
|
|
482
|
+
def main():
|
|
483
|
+
app()
|
|
332
484
|
|
|
333
485
|
|
|
334
486
|
if __name__ == "__main__":
|