buildai-cli 0.3.80__tar.gz → 0.3.82__tar.gz
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.
- {buildai_cli-0.3.80 → buildai_cli-0.3.82}/PKG-INFO +1 -1
- buildai_cli-0.3.82/cli/commands/grid.py +440 -0
- {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/main.py +3 -0
- {buildai_cli-0.3.80 → buildai_cli-0.3.82}/pyproject.toml +1 -1
- {buildai_cli-0.3.80 → buildai_cli-0.3.82}/.gitignore +0 -0
- {buildai_cli-0.3.80 → buildai_cli-0.3.82}/AGENTS.md +0 -0
- {buildai_cli-0.3.80 → buildai_cli-0.3.82}/CLAUDE.md +0 -0
- {buildai_cli-0.3.80 → buildai_cli-0.3.82}/buildai_bootstrap.py +0 -0
- {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/__init__.py +0 -0
- {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/_has_core.py +0 -0
- {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/auth_local.py +0 -0
- {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/commands/__init__.py +0 -0
- {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/commands/api_proxy.py +0 -0
- {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/commands/auth.py +0 -0
- {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/commands/db/__init__.py +0 -0
- {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/commands/db/broker.py +0 -0
- {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/commands/db/common.py +0 -0
- {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/commands/db/migrate.py +0 -0
- {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/commands/db/query.py +0 -0
- {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/commands/db/schema.py +0 -0
- {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/commands/db/status.py +0 -0
- {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/commands/db/tunnel.py +0 -0
- {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/commands/dev.py +0 -0
- {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/commands/doctor.py +0 -0
- {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/commands/egoexo.py +0 -0
- {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/commands/gigcamera.py +0 -0
- {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/commands/processing.py +0 -0
- {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/config.py +0 -0
- {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/console.py +0 -0
- {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/context.py +0 -0
- {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/db_broker.py +0 -0
- {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/guard.py +0 -0
- {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/internal_api.py +0 -0
- {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/nl_query/__init__.py +0 -0
- {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/nl_query/dataset_tools.py +0 -0
- {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/ops_init.py +0 -0
- {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/output.py +0 -0
- {buildai_cli-0.3.80 → buildai_cli-0.3.82}/cli/pagination.py +0 -0
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
"""Workspace commands for the GCS-native Build AI grid builder."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
from os import getenv
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Literal
|
|
9
|
+
from uuid import uuid4
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
|
|
13
|
+
from cli.console import error, info, success
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(
|
|
16
|
+
help="Build manifest-native video grids in gs://buildai-grids.", no_args_is_help=True
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
ZoomMode = Literal["auto", "always", "never"]
|
|
20
|
+
RunMode = Literal["local", "batch"]
|
|
21
|
+
DEFAULT_GRID_BUILDER_BATCH_IMAGE = (
|
|
22
|
+
"us-central1-docker.pkg.dev/data-470400/buildai-services/grid-builder:latest"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@app.command("validate")
|
|
27
|
+
def validate_manifest(
|
|
28
|
+
manifest: str = typer.Option(..., "--manifest", "-m", help="Local path or gs:// manifest URI."),
|
|
29
|
+
) -> None:
|
|
30
|
+
"""Validate a grid manifest without writing outputs."""
|
|
31
|
+
|
|
32
|
+
manifest_model, _state = _load_manifest(manifest)
|
|
33
|
+
success(
|
|
34
|
+
f"valid grid manifest: {manifest_model.grid_size}x{manifest_model.grid_size} "
|
|
35
|
+
f"({len(manifest_model.clips)} clips)"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@app.command("plan")
|
|
40
|
+
def plan_grid(
|
|
41
|
+
manifest: str = typer.Option(..., "--manifest", "-m", help="Local path or gs:// manifest URI."),
|
|
42
|
+
grid_key: str | None = typer.Option(
|
|
43
|
+
None, "--grid-key", help="Stable grid key for output prefix."
|
|
44
|
+
),
|
|
45
|
+
zoom_out: ZoomMode = typer.Option("auto", "--zoom-out", help="auto, always, or never."),
|
|
46
|
+
write: bool = typer.Option(False, "--write", help="Write normalized manifest and plan to GCS."),
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Create the deterministic render plan for a grid manifest."""
|
|
49
|
+
|
|
50
|
+
render_plan, state = _plan_from_manifest(
|
|
51
|
+
manifest, grid_key=grid_key, zoom_out=zoom_out, pin_sources=write
|
|
52
|
+
)
|
|
53
|
+
_print_plan(render_plan)
|
|
54
|
+
if write:
|
|
55
|
+
_write_plan(render_plan, state)
|
|
56
|
+
success(f"wrote plan: {render_plan.render_prefix}/plan.json")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@app.command("build")
|
|
60
|
+
def build_grid(
|
|
61
|
+
manifest: str = typer.Option(..., "--manifest", "-m", help="Local path or gs:// manifest URI."),
|
|
62
|
+
grid_key: str | None = typer.Option(
|
|
63
|
+
None, "--grid-key", help="Stable grid key for output prefix."
|
|
64
|
+
),
|
|
65
|
+
zoom_out: ZoomMode = typer.Option("auto", "--zoom-out", help="auto, always, or never."),
|
|
66
|
+
mode: RunMode = typer.Option("batch", "--mode", help="local or batch."),
|
|
67
|
+
workers: int = typer.Option(
|
|
68
|
+
1,
|
|
69
|
+
"--workers",
|
|
70
|
+
"-w",
|
|
71
|
+
min=1,
|
|
72
|
+
help="Batch task count; local mode runs one resumable worker loop.",
|
|
73
|
+
),
|
|
74
|
+
work_dir: Path = typer.Option(
|
|
75
|
+
Path(".grid-builder-work"), "--work-dir", help="Local scratch directory."
|
|
76
|
+
),
|
|
77
|
+
encoder: str = typer.Option("hevc_nvenc", "--encoder", help="FFmpeg video encoder."),
|
|
78
|
+
image: str | None = typer.Option(
|
|
79
|
+
None,
|
|
80
|
+
"--image",
|
|
81
|
+
help="Batch worker image URI; defaults to the repo-owned grid-builder image.",
|
|
82
|
+
),
|
|
83
|
+
project_id: str | None = typer.Option(
|
|
84
|
+
None, "--project-id", help="GCP project for Batch submission."
|
|
85
|
+
),
|
|
86
|
+
region: str = typer.Option("us-central1", "--region", help="Batch region."),
|
|
87
|
+
service_account: str | None = typer.Option(
|
|
88
|
+
None, "--service-account", help="Batch worker service account."
|
|
89
|
+
),
|
|
90
|
+
network: str | None = typer.Option(None, "--network", help="Batch VPC network."),
|
|
91
|
+
subnetwork: str | None = typer.Option(None, "--subnetwork", help="Batch VPC subnetwork."),
|
|
92
|
+
write: bool = typer.Option(False, "--write", help="Required for GCS mutations and rendering."),
|
|
93
|
+
) -> None:
|
|
94
|
+
"""Write a plan and execute or submit the grid render."""
|
|
95
|
+
|
|
96
|
+
if not write:
|
|
97
|
+
error("grid build mutates gs://buildai-grids; rerun with --write")
|
|
98
|
+
raise typer.Exit(1)
|
|
99
|
+
|
|
100
|
+
render_plan, state = _plan_from_manifest(
|
|
101
|
+
manifest,
|
|
102
|
+
grid_key=grid_key,
|
|
103
|
+
zoom_out=zoom_out,
|
|
104
|
+
pin_sources=True,
|
|
105
|
+
workers=workers,
|
|
106
|
+
)
|
|
107
|
+
_write_plan(render_plan, state)
|
|
108
|
+
if mode == "batch":
|
|
109
|
+
job_name = _submit_batch(
|
|
110
|
+
render_plan=render_plan,
|
|
111
|
+
image=image,
|
|
112
|
+
project_id=project_id,
|
|
113
|
+
region=region,
|
|
114
|
+
service_account=service_account,
|
|
115
|
+
network=network,
|
|
116
|
+
subnetwork=subnetwork,
|
|
117
|
+
workers=workers,
|
|
118
|
+
encoder=encoder,
|
|
119
|
+
)
|
|
120
|
+
success(f"submitted Batch job: {job_name}")
|
|
121
|
+
else:
|
|
122
|
+
summary = _execute_local(render_plan, state, work_dir=work_dir, encoder=encoder)
|
|
123
|
+
success(
|
|
124
|
+
f"local render pass complete: {len(summary.completed)} completed, "
|
|
125
|
+
f"{len(summary.skipped)} skipped"
|
|
126
|
+
)
|
|
127
|
+
info(render_plan.render_prefix)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@app.command("resume")
|
|
131
|
+
def resume_grid(
|
|
132
|
+
render_prefix: str = typer.Argument(..., help="Render prefix under gs://buildai-grids."),
|
|
133
|
+
mode: RunMode = typer.Option("batch", "--mode", help="local or batch."),
|
|
134
|
+
workers: int = typer.Option(
|
|
135
|
+
1,
|
|
136
|
+
"--workers",
|
|
137
|
+
"-w",
|
|
138
|
+
min=1,
|
|
139
|
+
help="Batch task count; local mode runs one resumable worker loop.",
|
|
140
|
+
),
|
|
141
|
+
work_dir: Path = typer.Option(Path(".grid-builder-work"), "--work-dir"),
|
|
142
|
+
encoder: str = typer.Option("hevc_nvenc", "--encoder", help="FFmpeg video encoder."),
|
|
143
|
+
image: str | None = typer.Option(
|
|
144
|
+
None,
|
|
145
|
+
"--image",
|
|
146
|
+
help="Batch worker image URI; defaults to the repo-owned grid-builder image.",
|
|
147
|
+
),
|
|
148
|
+
project_id: str | None = typer.Option(
|
|
149
|
+
None, "--project-id", help="GCP project for Batch submission."
|
|
150
|
+
),
|
|
151
|
+
region: str = typer.Option("us-central1", "--region", help="Batch region."),
|
|
152
|
+
service_account: str | None = typer.Option(
|
|
153
|
+
None, "--service-account", help="Batch worker service account."
|
|
154
|
+
),
|
|
155
|
+
network: str | None = typer.Option(None, "--network", help="Batch VPC network."),
|
|
156
|
+
subnetwork: str | None = typer.Option(None, "--subnetwork", help="Batch VPC subnetwork."),
|
|
157
|
+
write: bool = typer.Option(False, "--write", help="Required for GCS mutations and rendering."),
|
|
158
|
+
) -> None:
|
|
159
|
+
"""Resume a previously planned render, adopting completed receipts."""
|
|
160
|
+
|
|
161
|
+
if not write:
|
|
162
|
+
error("grid resume mutates gs://buildai-grids; rerun with --write")
|
|
163
|
+
raise typer.Exit(1)
|
|
164
|
+
state = _service("GcsState")()
|
|
165
|
+
render_plan = _load_plan_from_prefix(render_prefix, state)
|
|
166
|
+
if mode == "batch":
|
|
167
|
+
job_name = _submit_batch(
|
|
168
|
+
render_plan=render_plan,
|
|
169
|
+
image=image,
|
|
170
|
+
project_id=project_id,
|
|
171
|
+
region=region,
|
|
172
|
+
service_account=service_account,
|
|
173
|
+
network=network,
|
|
174
|
+
subnetwork=subnetwork,
|
|
175
|
+
workers=workers,
|
|
176
|
+
encoder=encoder,
|
|
177
|
+
)
|
|
178
|
+
success(f"submitted Batch job: {job_name}")
|
|
179
|
+
else:
|
|
180
|
+
summary = _execute_local(render_plan, state, work_dir=work_dir, encoder=encoder)
|
|
181
|
+
success(
|
|
182
|
+
f"local resume pass complete: {len(summary.completed)} completed, "
|
|
183
|
+
f"{len(summary.skipped)} skipped"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@app.command("status")
|
|
188
|
+
def grid_status(
|
|
189
|
+
render_prefix: str = typer.Argument(..., help="Render prefix under gs://buildai-grids."),
|
|
190
|
+
) -> None:
|
|
191
|
+
"""Show receipt status for a planned render."""
|
|
192
|
+
|
|
193
|
+
state = _service("GcsState")()
|
|
194
|
+
render_plan = _load_plan_from_prefix(render_prefix, state)
|
|
195
|
+
executor_cls = _service("GridTaskExecutor")
|
|
196
|
+
executor = executor_cls(
|
|
197
|
+
plan=render_plan,
|
|
198
|
+
state=state,
|
|
199
|
+
work_dir=Path(".grid-builder-status"),
|
|
200
|
+
owner="status",
|
|
201
|
+
handler=_noop_handler,
|
|
202
|
+
)
|
|
203
|
+
complete = sum(1 for task in render_plan.tasks if executor.receipt_is_valid(task))
|
|
204
|
+
pending = len(render_plan.tasks) - complete
|
|
205
|
+
info(f"render: {render_plan.render_id}")
|
|
206
|
+
info(f"grid: {render_plan.grid_key}")
|
|
207
|
+
info(f"tasks: {complete} complete, {pending} pending")
|
|
208
|
+
info(f"zoom_out: {'enabled' if render_plan.zoom_out_enabled else 'disabled'}")
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@app.command("verify")
|
|
212
|
+
def verify_grid(
|
|
213
|
+
render_prefix: str = typer.Argument(..., help="Render prefix under gs://buildai-grids."),
|
|
214
|
+
) -> None:
|
|
215
|
+
"""Verify that every planned task has a valid receipt."""
|
|
216
|
+
|
|
217
|
+
state = _service("GcsState")()
|
|
218
|
+
render_plan = _load_plan_from_prefix(render_prefix, state)
|
|
219
|
+
executor_cls = _service("GridTaskExecutor")
|
|
220
|
+
executor = executor_cls(
|
|
221
|
+
plan=render_plan,
|
|
222
|
+
state=state,
|
|
223
|
+
work_dir=Path(".grid-builder-verify"),
|
|
224
|
+
owner="verify",
|
|
225
|
+
handler=_noop_handler,
|
|
226
|
+
)
|
|
227
|
+
missing = [task.task_id for task in render_plan.tasks if not executor.receipt_is_valid(task)]
|
|
228
|
+
if missing:
|
|
229
|
+
error(f"{len(missing)} tasks missing valid receipts")
|
|
230
|
+
for task_id in missing[:20]:
|
|
231
|
+
error(f" {task_id}")
|
|
232
|
+
raise typer.Exit(1)
|
|
233
|
+
success("all grid builder task receipts are valid")
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _load_manifest(manifest_uri: str, state=None):
|
|
237
|
+
"""Load a local or GCS manifest while keeping service imports lazy."""
|
|
238
|
+
|
|
239
|
+
GridManifest = _service("GridManifest")
|
|
240
|
+
if manifest_uri.startswith("gs://"):
|
|
241
|
+
state = state or _service("GcsState")()
|
|
242
|
+
return GridManifest.model_validate(state.read_json(manifest_uri)), state
|
|
243
|
+
return GridManifest.model_validate_json(Path(manifest_uri).read_text(encoding="utf-8")), state
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _plan_from_manifest(
|
|
247
|
+
manifest_uri: str,
|
|
248
|
+
*,
|
|
249
|
+
grid_key: str | None,
|
|
250
|
+
zoom_out: ZoomMode,
|
|
251
|
+
pin_sources: bool,
|
|
252
|
+
workers: int = 1,
|
|
253
|
+
):
|
|
254
|
+
"""Normalize a manifest and build a render plan."""
|
|
255
|
+
|
|
256
|
+
state = _service("GcsState")() if pin_sources or manifest_uri.startswith("gs://") else None
|
|
257
|
+
manifest_model, state = _load_manifest(manifest_uri, state=state)
|
|
258
|
+
normalize_manifest = _service("normalize_manifest")
|
|
259
|
+
build_render_plan = _service("build_render_plan")
|
|
260
|
+
RenderProfile = _service("RenderProfile")
|
|
261
|
+
ZoomOutMode = _service("ZoomOutMode")
|
|
262
|
+
resolver = state if pin_sources else None
|
|
263
|
+
normalized = normalize_manifest(manifest_model, resolver=resolver, grid_key=grid_key)
|
|
264
|
+
profile = RenderProfile(zoom_out_mode=ZoomOutMode(zoom_out), worker_count=workers)
|
|
265
|
+
return build_render_plan(normalized, profile), state
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _write_plan(render_plan, state) -> None:
|
|
269
|
+
"""Write immutable plan sidecars under the render prefix."""
|
|
270
|
+
|
|
271
|
+
if state is None:
|
|
272
|
+
raise RuntimeError("GCS state is required before writing a grid render plan")
|
|
273
|
+
sidecars = {
|
|
274
|
+
"manifest.normalized.json": render_plan.manifest,
|
|
275
|
+
"profile.json": render_plan.profile,
|
|
276
|
+
"plan.json": render_plan,
|
|
277
|
+
"metadata/status.json": {
|
|
278
|
+
"schema_key": "buildai.grid_builder.status",
|
|
279
|
+
"schema_version": "1.0",
|
|
280
|
+
"render_id": render_plan.render_id,
|
|
281
|
+
"grid_key": render_plan.grid_key,
|
|
282
|
+
"task_count": len(render_plan.tasks),
|
|
283
|
+
"zoom_out_enabled": render_plan.zoom_out_enabled,
|
|
284
|
+
},
|
|
285
|
+
}
|
|
286
|
+
for relative_path, payload in sidecars.items():
|
|
287
|
+
try:
|
|
288
|
+
state.upload_json(
|
|
289
|
+
f"{render_plan.render_prefix}/{relative_path}", payload, create_only=True
|
|
290
|
+
)
|
|
291
|
+
except FileExistsError:
|
|
292
|
+
pass
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _execute_local(render_plan, state, *, work_dir: Path, encoder: str):
|
|
296
|
+
"""Run the worker loop locally for a render plan."""
|
|
297
|
+
|
|
298
|
+
if state is None:
|
|
299
|
+
raise RuntimeError("GCS state is required before executing a grid render")
|
|
300
|
+
GridTaskExecutor = _service("GridTaskExecutor")
|
|
301
|
+
GridRenderHandler = _service("GridRenderHandler")
|
|
302
|
+
executor = GridTaskExecutor(
|
|
303
|
+
plan=render_plan,
|
|
304
|
+
state=state,
|
|
305
|
+
work_dir=work_dir / render_plan.render_id,
|
|
306
|
+
owner="local#0",
|
|
307
|
+
handler=GridRenderHandler(encoder=encoder),
|
|
308
|
+
)
|
|
309
|
+
return executor.execute_until_complete(poll_interval_sec=2.0)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _load_plan_from_prefix(render_prefix: str, state):
|
|
313
|
+
"""Load a render plan from its GCS render prefix."""
|
|
314
|
+
|
|
315
|
+
RenderPlan = _service("RenderPlan")
|
|
316
|
+
return RenderPlan.model_validate(state.read_json(f"{render_prefix.rstrip('/')}/plan.json"))
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _submit_batch(
|
|
320
|
+
*,
|
|
321
|
+
render_plan,
|
|
322
|
+
image: str | None,
|
|
323
|
+
project_id: str | None,
|
|
324
|
+
region: str,
|
|
325
|
+
service_account: str | None,
|
|
326
|
+
network: str | None,
|
|
327
|
+
subnetwork: str | None,
|
|
328
|
+
workers: int,
|
|
329
|
+
encoder: str,
|
|
330
|
+
) -> str:
|
|
331
|
+
"""Submit a Cloud Batch worker fleet for a grid render plan."""
|
|
332
|
+
|
|
333
|
+
resolved_image = _grid_builder_batch_image(image)
|
|
334
|
+
if not all([project_id, service_account, network, subnetwork]):
|
|
335
|
+
error("--mode=batch requires --project-id, --service-account, --network, and --subnetwork")
|
|
336
|
+
raise typer.Exit(1)
|
|
337
|
+
from infra.batch import BatchClient, BatchJobConfig, ProvisioningModel
|
|
338
|
+
|
|
339
|
+
job_id = _batch_job_id(render_plan.render_id)
|
|
340
|
+
client = BatchClient(
|
|
341
|
+
project_id=project_id,
|
|
342
|
+
region=region,
|
|
343
|
+
vpc_network=network,
|
|
344
|
+
vpc_subnetwork=subnetwork,
|
|
345
|
+
service_account=service_account,
|
|
346
|
+
)
|
|
347
|
+
config = BatchJobConfig(
|
|
348
|
+
image=resolved_image,
|
|
349
|
+
entrypoint="python",
|
|
350
|
+
commands=[
|
|
351
|
+
"-m",
|
|
352
|
+
"services.grid_builder",
|
|
353
|
+
"worker",
|
|
354
|
+
"--plan-uri",
|
|
355
|
+
f"{render_plan.render_prefix}/plan.json",
|
|
356
|
+
"--work-dir",
|
|
357
|
+
"/tmp/grid-builder",
|
|
358
|
+
"--encoder",
|
|
359
|
+
encoder,
|
|
360
|
+
],
|
|
361
|
+
machine_type="g2-standard-4",
|
|
362
|
+
gpu_type="nvidia-l4",
|
|
363
|
+
gpu_count=1,
|
|
364
|
+
cpu_milli=4000,
|
|
365
|
+
memory_mib=16384,
|
|
366
|
+
boot_disk_gb=500,
|
|
367
|
+
service_account=service_account,
|
|
368
|
+
)
|
|
369
|
+
return client.create_job(
|
|
370
|
+
job_id=job_id,
|
|
371
|
+
task_count=workers,
|
|
372
|
+
config=config,
|
|
373
|
+
provisioning_model=ProvisioningModel.SPOT,
|
|
374
|
+
labels={"buildai-grid": "true", "render-id": render_plan.render_id},
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _grid_builder_batch_image(image: str | None) -> str:
|
|
379
|
+
"""Resolve the worker image while allowing operator overrides."""
|
|
380
|
+
|
|
381
|
+
return image or getenv("GRID_BUILDER_BATCH_IMAGE") or DEFAULT_GRID_BUILDER_BATCH_IMAGE
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _print_plan(render_plan) -> None:
|
|
385
|
+
"""Print the compact plan summary users need before launch."""
|
|
386
|
+
|
|
387
|
+
info(f"render_id: {render_plan.render_id}")
|
|
388
|
+
info(f"render_prefix: {render_plan.render_prefix}")
|
|
389
|
+
info(f"lod_sizes: {', '.join(str(size) for size in render_plan.lod_sizes)}")
|
|
390
|
+
info(f"zoom_out: {'enabled' if render_plan.zoom_out_enabled else 'disabled'}")
|
|
391
|
+
info(f"tasks: {len(render_plan.tasks)}")
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _noop_handler(_task, _context) -> Path:
|
|
395
|
+
"""Satisfy executor construction for receipt-only status commands."""
|
|
396
|
+
|
|
397
|
+
raise RuntimeError("status and verify commands do not execute render tasks")
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _service(name: str):
|
|
401
|
+
"""Import service symbols lazily so the standalone CLI remains lightweight."""
|
|
402
|
+
|
|
403
|
+
try:
|
|
404
|
+
import services.grid_builder as grid_builder
|
|
405
|
+
except ImportError as exc:
|
|
406
|
+
error("grid commands require a workspace install: run with `uv run buildai` from the repo")
|
|
407
|
+
raise typer.Exit(1) from exc
|
|
408
|
+
|
|
409
|
+
if name in {
|
|
410
|
+
"GridManifest",
|
|
411
|
+
"RenderPlan",
|
|
412
|
+
"RenderProfile",
|
|
413
|
+
"build_render_plan",
|
|
414
|
+
"normalize_manifest",
|
|
415
|
+
}:
|
|
416
|
+
return getattr(grid_builder, name)
|
|
417
|
+
if name == "GcsState":
|
|
418
|
+
from services.grid_builder.gcs_state import GcsState
|
|
419
|
+
|
|
420
|
+
return GcsState
|
|
421
|
+
if name == "GridTaskExecutor":
|
|
422
|
+
from services.grid_builder.executor import GridTaskExecutor
|
|
423
|
+
|
|
424
|
+
return GridTaskExecutor
|
|
425
|
+
if name == "GridRenderHandler":
|
|
426
|
+
from services.grid_builder.render_tasks import GridRenderHandler
|
|
427
|
+
|
|
428
|
+
return GridRenderHandler
|
|
429
|
+
if name == "ZoomOutMode":
|
|
430
|
+
from services.grid_builder.planner import ZoomOutMode
|
|
431
|
+
|
|
432
|
+
return ZoomOutMode
|
|
433
|
+
raise KeyError(name)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _batch_job_id(render_id: str) -> str:
|
|
437
|
+
"""Return a unique Cloud Batch job id while preserving render traceability."""
|
|
438
|
+
|
|
439
|
+
attempt = datetime.now(UTC).strftime("%Y%m%d%H%M%S")
|
|
440
|
+
return f"grid-{render_id}-{attempt}-{uuid4().hex[:8]}"
|
|
@@ -38,6 +38,7 @@ def _bootstrap_repo_paths() -> None:
|
|
|
38
38
|
return
|
|
39
39
|
|
|
40
40
|
for package_root in (
|
|
41
|
+
repo_root,
|
|
41
42
|
repo_root / "core",
|
|
42
43
|
repo_root / "apps" / "buildai-api" / "shared",
|
|
43
44
|
):
|
|
@@ -319,10 +320,12 @@ def db_callback(
|
|
|
319
320
|
if has_core():
|
|
320
321
|
from cli.commands.db import app as db_app
|
|
321
322
|
from cli.commands.gigcamera import app as gigcamera_app
|
|
323
|
+
from cli.commands.grid import app as grid_app
|
|
322
324
|
|
|
323
325
|
db_app.callback()(db_callback)
|
|
324
326
|
app.add_typer(db_app, name="db")
|
|
325
327
|
app.add_typer(gigcamera_app, name="gigcamera")
|
|
328
|
+
app.add_typer(grid_app, name="grid")
|
|
326
329
|
|
|
327
330
|
|
|
328
331
|
def main() -> None:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|