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
vlmparse/cli.py
CHANGED
|
@@ -1,334 +1,487 @@
|
|
|
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
|
+
server: Literal["registry", "hf"] = typer.Option(
|
|
19
|
+
"registry", help="Server 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
|
+
server=server,
|
|
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
|
+
server: 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
|
+
server: Server type for the model. Defaults to 'registry'.
|
|
131
|
+
with_vllm_server: Deprecated. Use --server hf instead. 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.
|
|
132
|
+
dpi: DPI to use for the conversion. If not specified, the default DPI will be used.
|
|
133
|
+
debug: If True, run in debug mode (single-threaded, no concurrency)
|
|
134
|
+
"""
|
|
135
|
+
from vlmparse.converter_with_server import ConverterWithServer
|
|
136
|
+
|
|
137
|
+
if with_vllm_server and server == "registry":
|
|
138
|
+
server = "hf"
|
|
139
|
+
|
|
140
|
+
with ConverterWithServer(
|
|
141
|
+
model=model,
|
|
142
|
+
uri=uri,
|
|
143
|
+
gpus=gpus,
|
|
144
|
+
server=server,
|
|
145
|
+
concurrency=concurrency,
|
|
146
|
+
return_documents=_return_documents,
|
|
147
|
+
) as converter_with_server:
|
|
148
|
+
return converter_with_server.parse(
|
|
149
|
+
inputs=inputs,
|
|
150
|
+
out_folder=out_folder,
|
|
151
|
+
mode=mode,
|
|
152
|
+
conversion_mode=conversion_mode,
|
|
153
|
+
dpi=dpi,
|
|
154
|
+
debug=debug,
|
|
37
155
|
)
|
|
38
156
|
|
|
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
157
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
158
|
+
@app.command("list")
|
|
159
|
+
def containers():
|
|
160
|
+
"""List all containers whose name begins with vlmparse."""
|
|
161
|
+
import docker
|
|
103
162
|
|
|
104
|
-
|
|
105
|
-
client = docker.from_env()
|
|
106
|
-
containers = client.containers.list()
|
|
163
|
+
from vlmparse.servers.utils import _get_container_labels
|
|
107
164
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
165
|
+
try:
|
|
166
|
+
client = docker.from_env()
|
|
167
|
+
all_containers = client.containers.list(all=True)
|
|
111
168
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
for container in containers
|
|
116
|
-
if container.name.startswith("vlmparse")
|
|
117
|
-
]
|
|
169
|
+
if not all_containers:
|
|
170
|
+
logger.info("No containers found")
|
|
171
|
+
return
|
|
118
172
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
return
|
|
173
|
+
# Group containers by compose project or as standalone
|
|
174
|
+
projects = {} # project_name -> list of containers
|
|
122
175
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
176
|
+
for container in all_containers:
|
|
177
|
+
labels = _get_container_labels(container)
|
|
178
|
+
project = labels.get("com.docker.compose.project") or labels.get(
|
|
179
|
+
"vlmparse_compose_project"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Include if name starts with vlmparse OR if it's in a vlmparse compose project
|
|
183
|
+
if container.name.startswith("vlmparse"):
|
|
184
|
+
if project:
|
|
185
|
+
projects.setdefault(project, []).append(container)
|
|
186
|
+
else:
|
|
187
|
+
projects[container.name] = [container]
|
|
188
|
+
elif project and project.startswith("vlmparse"):
|
|
189
|
+
projects.setdefault(project, []).append(container)
|
|
190
|
+
|
|
191
|
+
if not projects:
|
|
192
|
+
logger.info("No vlmparse containers found")
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
# Prepare table data - one row per project/standalone container
|
|
196
|
+
table_data = []
|
|
197
|
+
for project_name, project_containers in projects.items():
|
|
198
|
+
# Find the main container with vlmparse labels
|
|
199
|
+
main_container = project_containers[0]
|
|
200
|
+
for c in project_containers:
|
|
201
|
+
labels = _get_container_labels(c)
|
|
202
|
+
if labels.get("vlmparse_uri"):
|
|
203
|
+
main_container = c
|
|
204
|
+
break
|
|
205
|
+
|
|
206
|
+
labels = _get_container_labels(main_container)
|
|
207
|
+
|
|
208
|
+
# Extract port mappings from all containers in the project
|
|
209
|
+
ports = []
|
|
210
|
+
for container in project_containers:
|
|
128
211
|
if container.ports:
|
|
129
212
|
for _, host_bindings in container.ports.items():
|
|
130
213
|
if host_bindings:
|
|
131
214
|
for binding in host_bindings:
|
|
132
215
|
ports.append(f"{binding['HostPort']}")
|
|
133
216
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
table_data.append(
|
|
139
|
-
[
|
|
140
|
-
container.name,
|
|
141
|
-
container.status,
|
|
142
|
-
port_str,
|
|
143
|
-
gpu,
|
|
144
|
-
uri,
|
|
145
|
-
]
|
|
146
|
-
)
|
|
217
|
+
port_str = ", ".join(sorted(set(ports))) if ports else "N/A"
|
|
218
|
+
uri = labels.get("vlmparse_uri", "N/A")
|
|
219
|
+
gpu = labels.get("vlmparse_gpus", "N/A")
|
|
147
220
|
|
|
148
|
-
#
|
|
149
|
-
|
|
221
|
+
# Get all statuses
|
|
222
|
+
statuses = list(set(c.status for c in project_containers))
|
|
223
|
+
status_str = (
|
|
224
|
+
statuses[0] if len(statuses) == 1 else f"mixed ({', '.join(statuses)})"
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# Name: show project name if compose, otherwise container name
|
|
228
|
+
is_compose = labels.get("com.docker.compose.project") or labels.get(
|
|
229
|
+
"vlmparse_compose_project"
|
|
230
|
+
)
|
|
231
|
+
name = project_name if is_compose else main_container.name
|
|
150
232
|
|
|
151
|
-
|
|
152
|
-
table = tabulate(table_data, headers=headers, tablefmt="grid")
|
|
233
|
+
table_data.append([name, status_str, port_str, gpu, uri])
|
|
153
234
|
|
|
154
|
-
|
|
155
|
-
|
|
235
|
+
# Display as table
|
|
236
|
+
from tabulate import tabulate
|
|
156
237
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
logger.error(
|
|
160
|
-
"Make sure Docker is running and you have the necessary permissions"
|
|
161
|
-
)
|
|
238
|
+
headers = ["Name", "Status", "Port(s)", "GPU", "URI"]
|
|
239
|
+
table = tabulate(table_data, headers=headers, tablefmt="grid")
|
|
162
240
|
|
|
163
|
-
|
|
164
|
-
|
|
241
|
+
total = sum(len(containers) for containers in projects.values())
|
|
242
|
+
logger.info(
|
|
243
|
+
f"\nFound {len(projects)} vlmparse deployment(s) ({total} container(s)):\n"
|
|
244
|
+
)
|
|
245
|
+
print(table)
|
|
165
246
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
247
|
+
except docker.errors.DockerException as e:
|
|
248
|
+
logger.error(f"Failed to connect to Docker: {e}")
|
|
249
|
+
logger.error(
|
|
250
|
+
"Make sure Docker is running and you have the necessary permissions"
|
|
251
|
+
)
|
|
170
252
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
253
|
+
|
|
254
|
+
@app.command("stop")
|
|
255
|
+
def stop(
|
|
256
|
+
container: str | None = typer.Argument(None, help="Container ID or name to stop"),
|
|
257
|
+
):
|
|
258
|
+
"""Stop a Docker container by its ID or name.
|
|
259
|
+
|
|
260
|
+
If the selected container belongs to a Docker Compose project, the whole
|
|
261
|
+
compose stack is stopped and removed.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
container: Container ID or name to stop. If not specified, automatically stops the container if only one vlmparse container is running.
|
|
265
|
+
"""
|
|
266
|
+
import docker
|
|
267
|
+
|
|
268
|
+
from vlmparse.servers.utils import _stop_compose_stack_for_container
|
|
269
|
+
|
|
270
|
+
try:
|
|
271
|
+
client = docker.from_env()
|
|
272
|
+
|
|
273
|
+
# If no container specified, try to auto-select
|
|
274
|
+
if container is None:
|
|
275
|
+
from vlmparse.servers.utils import _get_container_labels
|
|
276
|
+
|
|
277
|
+
all_containers = client.containers.list()
|
|
278
|
+
|
|
279
|
+
# Group containers by compose project or as standalone
|
|
280
|
+
projects = {} # project_name -> list of containers
|
|
281
|
+
for c in all_containers:
|
|
282
|
+
labels = _get_container_labels(c)
|
|
283
|
+
project = labels.get("com.docker.compose.project") or labels.get(
|
|
284
|
+
"vlmparse_compose_project"
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
# Include if name starts with vlmparse OR if it's in a vlmparse compose project
|
|
288
|
+
if c.name.startswith("vlmparse"):
|
|
289
|
+
if project:
|
|
290
|
+
projects.setdefault(project, []).append(c)
|
|
291
|
+
else:
|
|
292
|
+
projects[c.name] = [c]
|
|
293
|
+
elif project and project.startswith("vlmparse"):
|
|
294
|
+
projects.setdefault(project, []).append(c)
|
|
295
|
+
|
|
296
|
+
if len(projects) == 0:
|
|
297
|
+
logger.error("No vlmparse containers found")
|
|
298
|
+
return
|
|
299
|
+
elif len(projects) > 1:
|
|
300
|
+
logger.error(
|
|
301
|
+
f"Multiple vlmparse deployments found ({len(projects)}). "
|
|
302
|
+
"Please specify a container ID or name:"
|
|
303
|
+
)
|
|
304
|
+
for project_name, containers in projects.items():
|
|
305
|
+
if len(containers) > 1:
|
|
306
|
+
logger.info(
|
|
307
|
+
f" - {project_name} ({len(containers)} containers)"
|
|
308
|
+
)
|
|
309
|
+
else:
|
|
310
|
+
logger.info(
|
|
311
|
+
f" - {containers[0].name} ({containers[0].short_id})"
|
|
312
|
+
)
|
|
313
|
+
return
|
|
194
314
|
else:
|
|
195
|
-
#
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
315
|
+
# Only one project/deployment, pick any container from it
|
|
316
|
+
target_container = list(projects.values())[0][0]
|
|
317
|
+
else:
|
|
318
|
+
# Try to get the specified container
|
|
319
|
+
try:
|
|
320
|
+
target_container = client.containers.get(container)
|
|
321
|
+
except docker.errors.NotFound:
|
|
322
|
+
logger.error(f"Container not found: {container}")
|
|
323
|
+
return
|
|
324
|
+
|
|
325
|
+
# If the container is part of a docker-compose stack, bring the whole stack down.
|
|
326
|
+
if _stop_compose_stack_for_container(target_container):
|
|
327
|
+
return
|
|
328
|
+
|
|
329
|
+
# Stop + remove the container
|
|
330
|
+
logger.info(
|
|
331
|
+
f"Stopping container: {target_container.name} ({target_container.short_id})"
|
|
332
|
+
)
|
|
333
|
+
try:
|
|
206
334
|
target_container.stop()
|
|
207
|
-
|
|
335
|
+
except Exception:
|
|
336
|
+
pass
|
|
337
|
+
try:
|
|
338
|
+
target_container.remove(force=True)
|
|
339
|
+
except Exception:
|
|
340
|
+
pass
|
|
341
|
+
logger.info("✓ Container stopped and removed successfully")
|
|
342
|
+
|
|
343
|
+
except docker.errors.DockerException as e:
|
|
344
|
+
logger.error(f"Failed to connect to Docker: {e}")
|
|
345
|
+
logger.error(
|
|
346
|
+
"Make sure Docker is running and you have the necessary permissions"
|
|
347
|
+
)
|
|
208
348
|
|
|
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
349
|
|
|
215
|
-
|
|
216
|
-
|
|
350
|
+
@app.command("log")
|
|
351
|
+
def log(
|
|
352
|
+
container: str | None = typer.Argument(
|
|
353
|
+
None, help="Container ID or name. If not specified, auto-selects."
|
|
354
|
+
),
|
|
355
|
+
follow: bool = typer.Option(True, help="Follow log output"),
|
|
356
|
+
tail: int = typer.Option(500, help="Number of lines to show from the end"),
|
|
357
|
+
):
|
|
358
|
+
"""Show logs from a Docker container.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
container: Container ID or name. If not specified, automatically selects the container if only one vlmparse container is running.
|
|
362
|
+
follow: If True, follow log output (stream logs in real-time)
|
|
363
|
+
tail: Number of lines to show from the end of the logs
|
|
364
|
+
"""
|
|
365
|
+
import docker
|
|
366
|
+
|
|
367
|
+
try:
|
|
368
|
+
client = docker.from_env()
|
|
369
|
+
|
|
370
|
+
# If no container specified, try to auto-select
|
|
371
|
+
if container is None:
|
|
372
|
+
from vlmparse.servers.utils import _get_container_labels
|
|
373
|
+
|
|
374
|
+
all_containers = client.containers.list()
|
|
375
|
+
vlmparse_containers = []
|
|
376
|
+
|
|
377
|
+
for c in all_containers:
|
|
378
|
+
labels = _get_container_labels(c)
|
|
379
|
+
project = labels.get("com.docker.compose.project") or labels.get(
|
|
380
|
+
"vlmparse_compose_project"
|
|
381
|
+
)
|
|
217
382
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
import docker
|
|
383
|
+
# Include if name starts with vlmparse OR if it's in a vlmparse compose project
|
|
384
|
+
if c.name.startswith("vlmparse") or (
|
|
385
|
+
project and project.startswith("vlmparse")
|
|
386
|
+
):
|
|
387
|
+
vlmparse_containers.append(c)
|
|
224
388
|
|
|
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")
|
|
389
|
+
if len(vlmparse_containers) == 0:
|
|
390
|
+
logger.error("No vlmparse containers found")
|
|
391
|
+
return
|
|
392
|
+
elif len(vlmparse_containers) > 1:
|
|
393
|
+
logger.error(
|
|
394
|
+
f"Multiple vlmparse containers found ({len(vlmparse_containers)}). "
|
|
395
|
+
"Please specify a container ID or name:"
|
|
396
|
+
)
|
|
397
|
+
for c in vlmparse_containers:
|
|
398
|
+
logger.info(f" - {c.name} ({c.short_id})")
|
|
399
|
+
return
|
|
269
400
|
else:
|
|
270
|
-
|
|
271
|
-
|
|
401
|
+
target_container = vlmparse_containers[0]
|
|
402
|
+
logger.info(
|
|
403
|
+
f"Showing logs for: {target_container.name} ({target_container.short_id})"
|
|
404
|
+
)
|
|
405
|
+
else:
|
|
406
|
+
# Try to get the specified container
|
|
407
|
+
try:
|
|
408
|
+
target_container = client.containers.get(container)
|
|
409
|
+
except docker.errors.NotFound:
|
|
410
|
+
logger.error(f"Container not found: {container}")
|
|
411
|
+
return
|
|
272
412
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
logger.
|
|
276
|
-
|
|
277
|
-
|
|
413
|
+
# Get and display logs
|
|
414
|
+
if follow:
|
|
415
|
+
logger.info("Following logs (press Ctrl+C to stop)...")
|
|
416
|
+
try:
|
|
417
|
+
for log_line in target_container.logs(
|
|
418
|
+
stream=True, follow=True, tail=tail
|
|
419
|
+
):
|
|
420
|
+
print(log_line.decode("utf-8", errors="replace"), end="")
|
|
421
|
+
except KeyboardInterrupt:
|
|
422
|
+
logger.info("\nStopped following logs")
|
|
423
|
+
else:
|
|
424
|
+
logs = target_container.logs().decode("utf-8", errors="replace")
|
|
425
|
+
print(logs)
|
|
278
426
|
|
|
279
|
-
|
|
280
|
-
"
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
docker_config_registry,
|
|
427
|
+
except docker.errors.DockerException as e:
|
|
428
|
+
logger.error(f"Failed to connect to Docker: {e}")
|
|
429
|
+
logger.error(
|
|
430
|
+
"Make sure Docker is running and you have the necessary permissions"
|
|
284
431
|
)
|
|
285
432
|
|
|
286
|
-
client_models = sorted(converter_config_registry.list_models())
|
|
287
|
-
server_models = sorted(docker_config_registry.list_models())
|
|
288
433
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
434
|
+
@app.command("list-register")
|
|
435
|
+
def list_register():
|
|
436
|
+
"""List all model keys registered in client and server registries."""
|
|
437
|
+
from vlmparse.registries import converter_config_registry, docker_config_registry
|
|
292
438
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
print(f" - {model}")
|
|
439
|
+
client_models = sorted(converter_config_registry.list_models())
|
|
440
|
+
server_models = sorted(docker_config_registry.list_models())
|
|
296
441
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
442
|
+
print("\nClient Models Registry:")
|
|
443
|
+
for model in client_models:
|
|
444
|
+
print(f" - {model}")
|
|
300
445
|
|
|
301
|
-
|
|
446
|
+
print("\nServer Models Registry:")
|
|
447
|
+
for model in server_models:
|
|
448
|
+
print(f" - {model}")
|
|
302
449
|
|
|
303
|
-
from vlmparse.st_viewer.st_viewer import __file__ as st_viewer_file
|
|
304
|
-
from vlmparse.st_viewer.st_viewer import run_streamlit
|
|
305
450
|
|
|
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}")
|
|
451
|
+
@app.command("view")
|
|
452
|
+
def view(folder: str = typer.Argument(..., help="Folder to visualize with Streamlit")):
|
|
453
|
+
import subprocess
|
|
454
|
+
import sys
|
|
326
455
|
|
|
456
|
+
from streamlit import runtime
|
|
327
457
|
|
|
328
|
-
|
|
329
|
-
import
|
|
458
|
+
from vlmparse.st_viewer.st_viewer import __file__ as st_viewer_file
|
|
459
|
+
from vlmparse.st_viewer.st_viewer import run_streamlit
|
|
460
|
+
|
|
461
|
+
if runtime.exists():
|
|
462
|
+
run_streamlit(folder)
|
|
463
|
+
else:
|
|
464
|
+
try:
|
|
465
|
+
subprocess.run(
|
|
466
|
+
[
|
|
467
|
+
sys.executable,
|
|
468
|
+
"-m",
|
|
469
|
+
"streamlit",
|
|
470
|
+
"run",
|
|
471
|
+
st_viewer_file,
|
|
472
|
+
"--",
|
|
473
|
+
folder,
|
|
474
|
+
],
|
|
475
|
+
check=True,
|
|
476
|
+
)
|
|
477
|
+
except KeyboardInterrupt:
|
|
478
|
+
print("\nStreamlit app terminated by user.")
|
|
479
|
+
except subprocess.CalledProcessError as e:
|
|
480
|
+
print(f"Error while running Streamlit: {e}")
|
|
330
481
|
|
|
331
|
-
|
|
482
|
+
|
|
483
|
+
def main():
|
|
484
|
+
app()
|
|
332
485
|
|
|
333
486
|
|
|
334
487
|
if __name__ == "__main__":
|