From bd8a48ba74866428adb5427f27d82a6cf1a9eaec Mon Sep 17 00:00:00 2001 From: GuillaumePELLUET Date: Sun, 19 Jul 2026 17:06:22 +0200 Subject: [PATCH] Rename UI launch command; auto-open browser; update Colab links to wl-* - Add top-level `weightslab launch` (and bare `weightslab start`) to bring up the Studio UI; keep `weightslab ui launch` working as a legacy alias. - Auto-open the UI in the default browser after launch (best-effort; skipped in CI/headless/pytest; disable with WEIGHTSLAB_NO_BROWSER). - Update docs + README to the new `weightslab launch` command. - Point "Open in Colab" example links at the renamed wl-*.ipynb notebooks. - Update CLI unit tests for the new launch/start dispatch. --- README.md | 4 +- docs/_static/examples-gallery.js | 12 +- docs/_static/wl-ribbon.js | 2 +- docs/configuration.rst | 6 +- docs/examples/pytorch/classification.rst | 4 +- docs/examples/pytorch/clustering.rst | 4 +- docs/examples/pytorch/detection.rst | 4 +- docs/examples/pytorch/generation.rst | 2 +- docs/examples/pytorch/segmentation.rst | 4 +- docs/examples/usecases/lidar_detection.rst | 2 +- .../usecases/loss_shape_classification.rst | 4 +- docs/four_way_approach.rst | 2 +- docs/quickstart.rst | 6 +- docs/ultralytics.rst | 2 +- docs/usage/docker.rst | 12 +- docs/user_commands.rst | 24 ++-- tests/backend/test_ui_docker_bridge.py | 20 ++- weightslab/src.py | 4 +- weightslab/tunnel.py | 6 +- weightslab/ui_docker_bridge.py | 115 +++++++++++++----- 20 files changed, 156 insertions(+), 83 deletions(-) diff --git a/README.md b/README.md index c9165675..53d22a04 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ pip install weightslab ``` **2. Launch Studio** ```bash -weightslab ui launch # then open https://localhost:5173 πŸš€ +weightslab launch # then open https://localhost:5173 πŸš€ ``` **3. Wrap your training script** ```python @@ -117,7 +117,7 @@ For a detailed installation guide and advanced configuration → [Installatio 3. **Run your script, then launch the UI in a separate terminal:** ```bash python train.py - weightslab ui launch + weightslab launch ``` 4. **Open your browser** `https://localhost:5173` and inspect your training in real time. diff --git a/docs/_static/examples-gallery.js b/docs/_static/examples-gallery.js index c5a6a609..d610919b 100644 --- a/docs/_static/examples-gallery.js +++ b/docs/_static/examples-gallery.js @@ -20,7 +20,7 @@ desc: 'CNN digit classifier on MNIST. Register hyperparameters, monitor per-sample loss, and use the deny-aware sampler to focus on hard examples.', tags: ['classification', 'supervised', 'mnist', 'cnn'], url: 'examples/pytorch/classification.html', - colab: COLAB + 'PyTorch/ws-classification.ipynb' + colab: COLAB + 'PyTorch/wl-classification.ipynb' }, { badge: 'PyTorch', color: 'pytorch', @@ -28,7 +28,7 @@ desc: 'Per-pixel semantic segmentation with a UNet. Track per-sample IoU and visualise mask overlays directly in the studio.', tags: ['segmentation', 'semantic', 'bdd100k', 'masks', 'dense prediction'], url: 'examples/pytorch/segmentation.html', - colab: COLAB + 'PyTorch/ws-segmentation.ipynb' + colab: COLAB + 'PyTorch/wl-segmentation.ipynb' }, { badge: 'PyTorch', color: 'pytorch', @@ -36,7 +36,7 @@ desc: 'Bounding-box detection on Penn-Fudan pedestrians. Per-instance multi-index dataframe with (sample_id, annotation_id) keys.', tags: ['detection', 'object detection', 'bounding boxes', 'penn-fudan'], url: 'examples/pytorch/detection.html', - colab: COLAB + 'PyTorch/ws-detection.ipynb' + colab: COLAB + 'PyTorch/wl-detection.ipynb' }, { badge: 'PyTorch', color: 'pytorch', @@ -44,7 +44,7 @@ desc: 'Metric learning with triplet loss on face datasets. Store and explore high-dimensional embeddings per sample in the studio.', tags: ['clustering', 'unsupervised', 'embeddings', 'face recognition', 'metric learning'], url: 'examples/pytorch/clustering.html', - colab: COLAB + 'PyTorch/ws-clustering.ipynb' + colab: COLAB + 'PyTorch/wl-clustering.ipynb' }, { badge: 'PyTorch', color: 'pytorch', @@ -52,7 +52,7 @@ desc: 'Unsupervised anomaly detection on MVTec with a multi-task UNet. Monitor reconstruction quality and per-sample anomaly scores.', tags: ['anomaly detection', 'generation', 'unsupervised', 'mvtec', 'reconstruction'], url: 'examples/pytorch/generation.html', - colab: COLAB + 'PyTorch/ws-generation.ipynb' + colab: COLAB + 'PyTorch/wl-generation.ipynb' }, { badge: 'Lightning', color: 'lightning', @@ -81,7 +81,7 @@ desc: 'Dynamic subscribed signal that classifies each sample\'s loss trajectory (monotonic, U-shape, spiked, …) and auto-tags it.', tags: ['loss analysis', 'signal', 'categorical tag', 'per-sample', 'trajectory'], url: 'examples/usecases/loss_shape_classification.html', - colab: COLAB + 'Usecases/ws-segmentation-loss-shapes.ipynb' + colab: COLAB + 'Usecases/wl-segmentation-loss-shapes-classification.ipynb' } ]; diff --git a/docs/_static/wl-ribbon.js b/docs/_static/wl-ribbon.js index 4fecb516..577c3366 100644 --- a/docs/_static/wl-ribbon.js +++ b/docs/_static/wl-ribbon.js @@ -10,7 +10,7 @@ 'The studio streams signals in real-time β€” no need to wait for an epoch to end to see results.', 'weightslab start example --cls launches a full MNIST classification demo in one command.', 'Use subscribe_to= on a signal to build reactive per-sample analytics derived from other signals.', - 'Run weightslab ui launch --certs to enable HTTPS + mTLS for secure remote studio access.', + 'Run weightslab launch --certs to enable HTTPS + mTLS for secure remote studio access.', 'Set preload_labels=False for large datasets to speed up startup; labels are loaded lazily.', 'Use array_return_proxies=True (default) to avoid loading the full dataset array into RAM.', 'Set WEIGHTSLAB_LOG_LEVEL=DEBUG to see full gRPC logs when debugging connectivity issues.', diff --git a/docs/configuration.rst b/docs/configuration.rst index 384a5733..287a2c59 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -354,7 +354,7 @@ Deploying the Studio ~~~~~~~~~~~~~~~~~~~~~ All Weights Studio configuration variables are passed to the UI at launch time -via ``weightslab ui launch``. There are two ways to supply them. +via ``weightslab launch``. There are two ways to supply them. **Option 1 β€” shell exports (quick, per-session)** @@ -362,7 +362,7 @@ via ``weightslab ui launch``. There are two ways to supply them. export ENABLE_AGENT=0 export BB_THUMB_RENDER=50 - weightslab ui launch + weightslab launch **Option 2 β€” ``.env`` file (persistent, version-controllable)** @@ -380,7 +380,7 @@ Then launch normally: .. code-block:: bash - weightslab ui launch + weightslab launch WeightsLab loads the ``.env`` file automatically. Shell exports take precedence over ``.env`` values. No rebuild of the Studio container is needed β€” variables diff --git a/docs/examples/pytorch/classification.rst b/docs/examples/pytorch/classification.rst index fafa0476..829f119b 100644 --- a/docs/examples/pytorch/classification.rst +++ b/docs/examples/pytorch/classification.rst @@ -157,14 +157,14 @@ debug values, custom distances, etc. .. code-block:: bash - weightslab ui launch # 1. deploy the studio + weightslab launch # 1. deploy the studio weightslab start example --cls # 2. start the classification demo .. raw:: html
- + Open In Colab
diff --git a/docs/examples/pytorch/clustering.rst b/docs/examples/pytorch/clustering.rst index 423e3449..af692ac3 100644 --- a/docs/examples/pytorch/clustering.rst +++ b/docs/examples/pytorch/clustering.rst @@ -93,14 +93,14 @@ silhouette score to identify consistently confused identities. .. code-block:: bash - weightslab ui launch # 1. deploy the studio + weightslab launch # 1. deploy the studio weightslab start example --clus # 2. start the clustering demo .. raw:: html
- + Open In Colab
diff --git a/docs/examples/pytorch/detection.rst b/docs/examples/pytorch/detection.rst index ab201c04..ba2f0668 100644 --- a/docs/examples/pytorch/detection.rst +++ b/docs/examples/pytorch/detection.rst @@ -125,14 +125,14 @@ pipeline. See :ref:`good-practice-get-items` for the recommended signature. .. code-block:: bash - weightslab ui launch # 1. deploy the studio + weightslab launch # 1. deploy the studio weightslab start example --det # 2. start the detection demo .. raw:: html
- + Open In Colab
diff --git a/docs/examples/pytorch/generation.rst b/docs/examples/pytorch/generation.rst index ebdf3d68..9ab356bd 100644 --- a/docs/examples/pytorch/generation.rst +++ b/docs/examples/pytorch/generation.rst @@ -99,5 +99,5 @@ filter by pair distance to find the hardest negatives. .. code-block:: bash - weightslab ui launch # 1. deploy the studio + weightslab launch # 1. deploy the studio weightslab start example --gen # 2. start the generation / anomaly demo diff --git a/docs/examples/pytorch/segmentation.rst b/docs/examples/pytorch/segmentation.rst index c5bb53cf..cdb66c8f 100644 --- a/docs/examples/pytorch/segmentation.rst +++ b/docs/examples/pytorch/segmentation.rst @@ -112,14 +112,14 @@ these signals over training steps and lets you sort samples by them. .. code-block:: bash - weightslab ui launch # 1. deploy the studio + weightslab launch # 1. deploy the studio weightslab start example --seg # 2. start the segmentation demo .. raw:: html
- + Open In Colab
diff --git a/docs/examples/usecases/lidar_detection.rst b/docs/examples/usecases/lidar_detection.rst index d7eec585..f03a5924 100644 --- a/docs/examples/usecases/lidar_detection.rst +++ b/docs/examples/usecases/lidar_detection.rst @@ -120,7 +120,7 @@ and override ``load_points`` and optionally ``render_thumbnail_2d``: .. code-block:: bash - weightslab ui launch # 1. deploy the studio + weightslab launch # 1. deploy the studio weightslab start example --2d_det # 2a. 2D pillar-grid detection # or weightslab start example --3d_det # 2b. 3D bounding-box detection diff --git a/docs/examples/usecases/loss_shape_classification.rst b/docs/examples/usecases/loss_shape_classification.rst index 822bef81..a2286b40 100644 --- a/docs/examples/usecases/loss_shape_classification.rst +++ b/docs/examples/usecases/loss_shape_classification.rst @@ -203,7 +203,7 @@ Workflow in the studio .. code-block:: bash - weightslab ui launch # 1. deploy the studio + weightslab launch # 1. deploy the studio weightslab start example --det # 2. start the detection demo To run the full loss-shape example (with the ``@wl.signal(subscribe_to=...)`` @@ -213,7 +213,7 @@ Workflow in the studio .. raw:: html
- + Open In Colab
diff --git a/docs/four_way_approach.rst b/docs/four_way_approach.rst index 9981dbbe..af4f3c53 100644 --- a/docs/four_way_approach.rst +++ b/docs/four_way_approach.rst @@ -52,7 +52,7 @@ Typical integration flow to custom Python script .. code-block:: bash - weightslab ui launch + weightslab launch python train.py - Resume training from the UI and use tags/discards/signals to iteratively improve data and model behavior. diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 9d24be8a..7014c492 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -44,7 +44,7 @@ Then, in another terminal, launch the UI and open http://localhost:5173: .. code-block:: bash - weightslab ui launch + weightslab launch .. Launch WeightsLab services from your training script @@ -139,8 +139,8 @@ Pass ``--certs`` to generate (if missing) and use TLS certificates + a gRPC auth .. code-block:: bash - weightslab ui launch # unsecured HTTP (default; no certs generated) - weightslab ui launch --certs # secured HTTPS + gRPC auth (generates certs if missing) + weightslab launch # unsecured HTTP (default; no certs generated) + weightslab launch --certs # secured HTTPS + gRPC auth (generates certs if missing) .. important:: diff --git a/docs/ultralytics.rst b/docs/ultralytics.rst index 0dc5a5aa..eb0a57a4 100644 --- a/docs/ultralytics.rst +++ b/docs/ultralytics.rst @@ -201,7 +201,7 @@ Running the bundled example .. code-block:: bash - weightslab ui launch + weightslab launch 4. Run the example: diff --git a/docs/usage/docker.rst b/docs/usage/docker.rst index 26beb6d8..c32ae292 100644 --- a/docs/usage/docker.rst +++ b/docs/usage/docker.rst @@ -27,8 +27,8 @@ Weights Studio frontend alongside it. - Siblings on the host - Siblings on the host * - Starts UI via - - ``weightslab ui launch`` - - ``weightslab ui launch`` + - ``weightslab launch`` + - ``weightslab launch`` - Custom ``ui-compose.yml`` * - ``--privileged`` - **Required** @@ -59,7 +59,7 @@ Weights Studio frontend alongside it. - **A (DinD)** β€” fully self-contained; needs ``--privileged``. Best when you want isolation or are on Windows. - - **B (DooD)** β€” uses the stock ``weightslab ui launch``; cleanest on a + - **B (DooD)** β€” uses the stock ``weightslab launch``; cleanest on a Linux host with path alignment already done. - **C (self-contained siblings)** β€” no host prep, no bind mounts, works natively on Windows. @@ -72,7 +72,7 @@ Option A: Docker-in-Docker (DinD) **Source:** ``weightslab/examples/Docker_training/docker_in_docker/`` The trainer container starts its **own inner Docker daemon** and uses -``weightslab ui launch`` inside it. Because the inner daemon shares the +``weightslab launch`` inside it. Because the inner daemon shares the trainer container's filesystem, all paths resolve without any host-side setup. Wiring diagram @@ -183,7 +183,7 @@ Open http://localhost:5173. Stop with ``Ctrl+C`` or ``docker compose down``. What ``WEIGHTSLAB_TLS=1`` does in ``entrypoint.sh``: - 1. Runs ``weightslab ui launch --certs``, generating certs into + 1. Runs ``weightslab launch --certs``, generating certs into ``WEIGHTSLAB_CERTS_DIR`` and configuring Envoy + the frontend for HTTPS. 2. Exports ``GRPC_TLS_ENABLED=1`` + ``GRPC_TLS_CERT_DIR`` so the gRPC backend also speaks TLS (required to prevent Envoy upstream 503s). @@ -252,7 +252,7 @@ Configuration requirements How config delivery works without bind mounts ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Option C does not call ``weightslab ui launch``. Instead, ``entrypoint.sh``: +Option C does not call ``weightslab launch``. Instead, ``entrypoint.sh``: 1. Renders Envoy's plaintext config from the installed ``weightslab`` package. 2. Pipes it into the ``wl_envoy_cfg`` named volume via the socket using a diff --git a/docs/user_commands.rst b/docs/user_commands.rst index ebe35fb4..03bef4bb 100644 --- a/docs/user_commands.rst +++ b/docs/user_commands.rst @@ -38,7 +38,7 @@ with no further arguments prints the banner and this same command summary. - Purpose * - ``weightslab se`` - One-time secure setup: generate TLS certs + a gRPC auth token. - * - ``weightslab ui launch`` + * - ``weightslab launch`` - Clean stale Docker state, then build & start the Weights Studio UI stack. * - ``weightslab start example`` (alias: ``weightslab example start``) - Run a bundled PyTorch example in the foreground. @@ -60,7 +60,7 @@ weightslab se Generates TLS certificates and a gRPC auth token into a certs directory, then tells you to export ``WEIGHTSLAB_CERTS_DIR`` β€” the **single source of -truth** the training backend, ``weightslab ui launch --certs``, and any new +truth** the training backend, ``weightslab launch --certs``, and any new shell all read to decide whether TLS/auth is on (derived purely from whether cert files exist in that directory). @@ -85,14 +85,14 @@ permanently (the command prints the exact ``export`` / ``setx`` line for your platform) so the training backend and Weights Studio agree on the same certificates. -weightslab ui launch +weightslab launch ~~~~~~~~~~~~~~~~~~~~~~ **Syntax** .. code-block:: bash - weightslab ui launch [certs_dir] [--certs] [-i/--image REPO] [-v/--version TAG] + weightslab launch [certs_dir] [--certs] [-i/--image REPO] [-v/--version TAG] Purges stale ``weightslab``/``weights_studio`` Docker resources scoped to the bundled stack, then builds and starts the Weights Studio UI via Docker @@ -116,10 +116,10 @@ Compose. .. code-block:: bash - weightslab ui launch # unsecured HTTP (default) - weightslab ui launch --certs # secured HTTPS + gRPC auth - weightslab ui launch -i guillaumep2705/weightslab # pull a custom repo (latest tag) - weightslab ui launch -i guillaumep2705/weightslab -v v1.2.3 # pin a specific version + weightslab launch # unsecured HTTP (default) + weightslab launch --certs # secured HTTPS + gRPC auth + weightslab launch -i guillaumep2705/weightslab # pull a custom repo (latest tag) + weightslab launch -i guillaumep2705/weightslab -v v1.2.3 # pin a specific version Once running, the UI is served at ``http://localhost:5173`` (or ``https://...`` when ``--certs`` is used); the exact URL is also printed at @@ -180,7 +180,7 @@ the documented form. weightslab start example --3d_det # 3D LiDAR detection weightslab example start --det # tolerant alias, same as `start example --det` -Then, in another terminal: ``weightslab ui launch`` and open +Then, in another terminal: ``weightslab launch`` and open ``http://localhost:5173``. See :doc:`examples/index` for what each example demonstrates. @@ -237,7 +237,7 @@ HTTP/2 frames must pass through untouched. Two consequences: zero-signup option is `bore `_ with its free public relay: ``bore local 50051 --to bore.pub`` (prints ``bore.pub:``). ``ngrok tcp 50051`` also works but now requires a credit card on the free tier. -- The backend must run **plaintext** β€” the default ``weightslab ui launch`` +- The backend must run **plaintext** β€” the default ``weightslab launch`` (no ``--certs``) β€” so no TLS terminates mid-path. **Arguments** @@ -274,7 +274,7 @@ HTTP/2 frames must pass through untouched. Two consequences: # !bore local 50051 --to bore.pub # 2) On your machine, in two terminals: - weightslab ui launch # plaintext HTTP (default) + weightslab launch # plaintext HTTP (default) weightslab tunnel bore.pub:12345 # the host:port bore printed # 3) Open http://localhost:5173 β€” Studio streams live from Colab. @@ -289,7 +289,7 @@ HTTP/2 frames must pass through untouched. Two consequences: The command probes the remote on startup (warning, not fatal, if it isn't up yet), re-resolves the endpoint per connection (so a changing tunnel IP is picked up), and runs until ``Ctrl+C``. See the classification Colab notebook -(``examples/Notebooks/PyTorch/ws-classification.ipynb``) for the end-to-end +(``examples/Notebooks/PyTorch/wl-classification.ipynb``) for the end-to-end setup. .. _cli-console: diff --git a/tests/backend/test_ui_docker_bridge.py b/tests/backend/test_ui_docker_bridge.py index bb2e29fe..4778240e 100644 --- a/tests/backend/test_ui_docker_bridge.py +++ b/tests/backend/test_ui_docker_bridge.py @@ -389,7 +389,8 @@ def test_ui_secure_environment_cert_failure(self, mock_gen_certs): class TestMainCLI(unittest.TestCase): - """The CLI exposes exactly: se, ui launch, start example, help.""" + """The CLI exposes: se, launch (+ bare start), start example, cli, tunnel, + help; the legacy `ui launch` still dispatches to the same handler.""" @patch("weightslab.ui_docker_bridge.ui_secure_environment") def test_main_dispatches_se(self, mock_se): @@ -413,9 +414,18 @@ def test_main_ui_without_action_does_not_crash(self): with patch("sys.argv", ["weightslab", "ui"]): main() # should print ui help, not raise - def test_main_start_without_target_does_not_crash(self): + @patch("weightslab.ui_docker_bridge.ui_launch") + def test_main_dispatches_launch(self, mock_launch): + with patch("sys.argv", ["weightslab", "launch"]): + main() + mock_launch.assert_called_once() + + @patch("weightslab.ui_docker_bridge.ui_launch") + def test_main_bare_start_launches_ui(self, mock_launch): + # bare `weightslab start` is an alias for `weightslab launch` with patch("sys.argv", ["weightslab", "start"]): - main() # should print start help, not raise + main() + mock_launch.assert_called_once() def test_main_help_does_not_crash(self): with patch("sys.argv", ["weightslab", "help"]): @@ -684,6 +694,10 @@ def test_no_args_shows_command_reference(self): out = self._capture_main(["weightslab"]) self.assertIn("ui launch", out) + def test_help_mentions_new_launch_command(self): + out = self._capture_main(["weightslab", "help"]) + self.assertIn("weightslab launch", out) + def test_help_does_not_mention_removed_commands(self): out = self._capture_main(["weightslab", "help"]) for removed in ("ui stop", "ui drop", "ui docker", "--no-auth", "--no-clean"): diff --git a/weightslab/src.py b/weightslab/src.py index c8b67a1e..69d29353 100644 --- a/weightslab/src.py +++ b/weightslab/src.py @@ -1504,7 +1504,7 @@ def serve(serving_cli: bool = True, serving_grpc: bool = False, `bore `_ raw-TCP relay, so a Weights Studio running on another machine can reach it (e.g. training in Google Colab, UI on your laptop). Prints the public endpoint and the - two commands to run locally (``weightslab ui launch`` + + two commands to run locally (``weightslab launch`` + ``weightslab tunnel ``). Implies a gRPC backend. Uses the shared public relay ``bore.pub`` β€” the random port is the only thing guarding it, so keep it to non-sensitive demos. @@ -1590,7 +1590,7 @@ def serve(serving_cli: bool = True, serving_grpc: bool = False, print("=" * 60) print(f" Backend exposed via bore at: {bore_endpoint}") print(" On your local machine (Docker running), run:") - print(" weightslab ui launch") + print(" weightslab launch") print(f" weightslab tunnel {bore_endpoint}") print("=" * 60) diff --git a/weightslab/tunnel.py b/weightslab/tunnel.py index ff768d38..e737ceed 100644 --- a/weightslab/tunnel.py +++ b/weightslab/tunnel.py @@ -5,7 +5,7 @@ stack connects to it as if it were local. The stack's Envoy proxy dials ``localhost:50051`` (see the ``grpc_service`` upstream in the bundled envoy config), so making the remote backend appear there needs no change to the UI at -all: just run ``weightslab ui launch`` and this tunnel side by side. +all: just run ``weightslab launch`` and this tunnel side by side. Why a *raw* byte forwarder (no protocol parsing): the browser speaks gRPC-Web to Envoy, and Envoy speaks native HTTP/2 gRPC to its upstream. Those HTTP/2 frames @@ -14,7 +14,7 @@ leaves the protocol untouched. The matching remote tunnel must likewise be raw TCP β€” ``bore local 50051 --to bore.pub`` (zero-signup) or ``ngrok tcp 50051`` (needs a card on the free tier) β€” and the backend must run **plaintext** (the -default ``weightslab ui launch`` β€” no ``--certs``) so no TLS terminates mid-path. +default ``weightslab launch`` β€” no ``--certs``) so no TLS terminates mid-path. """ import logging @@ -178,7 +178,7 @@ def run_tunnel(remote_host: str, remote_port: int, logger.info("=" * 60) logger.info(f" Tunnel up: {listen_host}:{listen_port} -> {remote_host}:{remote_port}") - logger.info(" If the UI isn't running yet, in another terminal: weightslab ui launch") + logger.info(" If the UI isn't running yet, in another terminal: weightslab launch") logger.info(" Then open http://localhost:5173") logger.info(" Ctrl+C to stop the tunnel.") logger.info("=" * 60) diff --git a/weightslab/ui_docker_bridge.py b/weightslab/ui_docker_bridge.py index 6ec8745d..11a1e826 100644 --- a/weightslab/ui_docker_bridge.py +++ b/weightslab/ui_docker_bridge.py @@ -148,9 +148,11 @@ def _banner() -> str: shells find them. --force-certs regenerate even if certs exist - ui launch Purge stale weightslab/weights_studio Docker - resources, then build & start the UI stack. - UNSECURED (HTTP) by default β€” no certs generated. + launch Purge stale weightslab/weights_studio Docker + resources, build & start the UI stack, then open it + in your browser. UNSECURED (HTTP) by default β€” no + certs generated. Also: bare `weightslab start`. + (Legacy alias: `weightslab ui launch`.) --certs generate (if missing) + use TLS certs + gRPC auth (HTTPS) @@ -176,7 +178,7 @@ def _banner() -> str: tunnel Forward a REMOTE gRPC backend (e.g. a Colab run behind `ngrok tcp 50051`) to a LOCAL port so the UI stack β€” whose Envoy dials localhost:50051 β€” reaches - it. Run alongside `weightslab ui launch`. Raw TCP, so + it. Run alongside `weightslab launch`. Raw TCP, so the backend must be plaintext (default launch). ENDPOINT remote host:port (e.g. bore.pub:12345) (default: $WEIGHTSLAB_TUNNEL_ENDPOINT) @@ -187,10 +189,12 @@ def _banner() -> str: examples: weightslab se # one-time secure setup (then export WEIGHTSLAB_CERTS_DIR) weightslab se --force-certs # regenerate the certs - weightslab ui launch # clean + launch (unsecured HTTP, default) - weightslab ui launch --certs # secured launch (HTTPS + gRPC auth) - weightslab ui launch -i guillaumep2705/weightslab # pull frontend from a custom repo (latest) - weightslab ui launch -i guillaumep2705/weightslab -v v1.2.3 # pin a specific version/tag + weightslab launch # clean + launch UI, open browser (unsecured HTTP, default) + weightslab start # same as `weightslab launch` + weightslab launch --certs # secured launch (HTTPS + gRPC auth) + weightslab launch -i guillaumep2705/weightslab # pull frontend from a custom repo (latest) + weightslab launch -i guillaumep2705/weightslab -v v1.2.3 # pin a specific version/tag + weightslab ui launch # legacy alias for `weightslab launch` weightslab start example # run the classification demo (default) weightslab start example --seg # run the segmentation demo weightslab start example --det # run the detection demo @@ -776,6 +780,29 @@ def _clean_stale_docker_resources(frontend_image: str = _FRONTEND_IMAGE) -> None logger.debug(f"Could not remove {env_file}: {exc}") +def _open_ui_in_browser(url: str) -> None: + """Best-effort: open the Studio UI in the default browser after launch. + + Skipped when WEIGHTSLAB_NO_BROWSER is set, in CI, or on a headless Linux box + (no DISPLAY / Wayland). Never raises β€” a failed open must not fail the launch. + """ + if (os.environ.get("WEIGHTSLAB_NO_BROWSER") + or os.environ.get("CI") + or os.environ.get("PYTEST_CURRENT_TEST")): + return + if sys.platform.startswith("linux") and not ( + os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY") + ): + logger.info("Headless environment β€” open the UI manually at %s", url) + return + try: + import webbrowser + if webbrowser.open_new_tab(url): + logger.info("Opening the Weights Studio UI in your browser…") + except Exception as e: # noqa: BLE001 β€” auto-open is best-effort only + logger.debug("Could not auto-open the browser: %s", e) + + def ui_launch(args): """Purge stale Docker state, then build & start the UI. @@ -937,7 +964,10 @@ def ui_launch(args): # file-presence rule the deploy pipeline applies. secured = manager.has_valid_certs() protocol = "https" if secured else "http" - logger.info(f"Weights Studio UI is running at: {protocol}://localhost:{port}") + url = f"{protocol}://localhost:{port}" + logger.info(f"Weights Studio UI is running at: {url}") + # Auto-open the UI (best-effort; set WEIGHTSLAB_NO_BROWSER to disable). + _open_ui_in_browser(url) if secured: certs_dir_str = str(manager.certs_dir) # Persist first (it logs its own lines), so the export/setx reminder @@ -955,7 +985,7 @@ def ui_launch(args): logger.warning(f" (Windows) setx WEIGHTSLAB_CERTS_DIR \"{certs_dir_str}\"") else: logger.info("UI is running UNSECURED (HTTP, no gRPC auth). " - "Re-run with `weightslab ui launch --certs` for TLS.") + "Re-run with `weightslab launch --certs` for TLS.") def ui_secure_environment(args): @@ -997,7 +1027,7 @@ def ui_secure_environment(args): logger.info(" gRPC auth token created") logger.info(f" Certs and token stored in: {manager.certs_dir}") logger.info(f" WEIGHTSLAB_CERTS_DIR exported for this process: {manager.certs_dir}") - logger.info("Then launch the secured UI with: weightslab ui launch --certs") + logger.info("Then launch the secured UI with: weightslab launch --certs") # Keep this the FINAL output so the user can't miss the action they must take. logger.warning("") logger.warning(" ACTION REQUIRED β€” set WEIGHTSLAB_CERTS_DIR globally so new shells " @@ -1075,7 +1105,7 @@ def example_start(args): logger.info(f"Starting the WeightsLab {label} ({kind}) example...") logger.info(f" {main_py}") - logger.info("In another terminal, launch the UI with: weightslab ui launch") + logger.info("In another terminal, launch the UI with: weightslab launch") logger.info(f"Then open http://localhost:5173 β€” stop the example with Ctrl+C.") if not _CERTS_DIR_IN_ORIGINAL_ENV: manager = CertAuthManager.from_env_or_default() @@ -1136,15 +1166,30 @@ def _add_example_kind_flags(p: argparse.ArgumentParser) -> None: p.set_defaults(example_kind=_DEFAULT_EXAMPLE) +def _add_launch_flags(p, with_certs_dir: bool = True) -> None: + """Shared flags for every UI-launch entry point: `weightslab launch`, the bare + `weightslab start`, and the legacy `weightslab ui launch`.""" + p.add_argument('--certs', action='store_true', + help='Generate (if missing) and use TLS certs + gRPC auth token (secured HTTPS). Default: unsecured HTTP.') + p.add_argument('-i', '--image', default=None, + help='Frontend image repo to run/pull (default: graybx/weightslab). e.g. guillaumep2705/weightslab') + p.add_argument('-v', '--version', default=None, + help='Frontend image tag/version to pull (default: latest). e.g. v1.2.3') + if with_certs_dir: + p.add_argument('certs_dir', nargs='?', default=None, + help='Custom directory for certs/token (default: $WEIGHTSLAB_CERTS_DIR or ~/.weightslab-certs)') + + def _build_parser() -> argparse.ArgumentParser: """Build the top-level argument parser (banner + detailed command reference). The CLI is intentionally minimal β€” exactly these commands: weightslab --help | -h | help weightslab se [--force-certs] - weightslab ui launch [--certs] + weightslab launch [--certs] (also: bare `weightslab start`) weightslab start example [--cls|--seg|--det|--clus|--gen|--3d_det|--2d_det] weightslab cli [--port PORT] [--host HOST] + weightslab ui launch [--certs] (legacy alias of `launch`) """ parser = argparse.ArgumentParser( prog="weightslab", @@ -1154,7 +1199,7 @@ def _build_parser() -> argparse.ArgumentParser: ) # metavar lists only the documented commands; the `example` alias is accepted # but intentionally omitted here (and help=SUPPRESS'd below) so it stays hidden. - sub = parser.add_subparsers(dest="command", metavar="{se,ui,start,cli,tunnel,help}") + sub = parser.add_subparsers(dest="command", metavar="{se,launch,start,ui,cli,tunnel,help}") # weightslab se [--force-certs] [certs_dir] se_parser = sub.add_parser("se", help="Set up the secure environment (TLS certs + gRPC auth token)") @@ -1162,19 +1207,23 @@ def _build_parser() -> argparse.ArgumentParser: se_parser.add_argument('certs_dir', nargs='?', default=None, help='Custom directory for certs/token (default: $WEIGHTSLAB_CERTS_DIR or ~/.weightslab-certs)') - # weightslab ui launch [--certs] [certs_dir] - ui_parser = sub.add_parser("ui", help="Manage the Weights Studio UI") + # weightslab launch [--certs] [-i IMAGE] [-v VERSION] [certs_dir] + # Primary command to bring up the Weights Studio UI (also reachable as the + # bare `weightslab start`, and the legacy `weightslab ui launch`). + launch_parser = sub.add_parser( + "launch", + help="Clean stale Docker state, launch the UI, and open it in the browser " + "(unsecured by default; --certs for TLS)") + _add_launch_flags(launch_parser) + + # weightslab ui launch [...] β€” LEGACY alias, kept working for backward + # compatibility (existing notebooks/docs). Prefer `weightslab launch`. + # (help=SUPPRESS hides the `ui` command from the top-level --help listing.) + ui_parser = sub.add_parser("ui", help=argparse.SUPPRESS) ui_sub = ui_parser.add_subparsers(dest="action") launch_ui_parser = ui_sub.add_parser( - "launch", help="Clean stale Docker state, then launch the UI (unsecured by default; --certs for TLS)") - launch_ui_parser.add_argument('--certs', action='store_true', help='Generate (if missing) and use TLS certs + gRPC auth token (secured HTTPS). Default: unsecured HTTP.') - launch_ui_parser.add_argument('-i', '--image', default=None, - help='Frontend image repo to run/pull (default: graybx/weightslab). ' - 'e.g. guillaumep2705/weightslab') - launch_ui_parser.add_argument('-v', '--version', default=None, - help='Frontend image tag/version to pull (default: latest). e.g. v1.2.3') - launch_ui_parser.add_argument('certs_dir', nargs='?', default=None, - help='Custom directory for certs/token (default: $WEIGHTSLAB_CERTS_DIR or ~/.weightslab-certs)') + "launch", help="(legacy) alias for `weightslab launch`") + _add_launch_flags(launch_ui_parser) # weightslab cli [--port N] [--host H] cli_parser = sub.add_parser( @@ -1203,8 +1252,12 @@ def _build_parser() -> argparse.ArgumentParser: '--remote-port', type=int, default=None, help="Remote port, if not included in ENDPOINT") - # weightslab start example [--cls|--seg|--clus|--gen] - start_parser = sub.add_parser("start", help="Start a bundled WeightsLab resource") + # weightslab start [--certs ...] β†’ launch the UI (alias of `launch`) + # weightslab start example [--cls|...] β†’ run a bundled PyTorch example + start_parser = sub.add_parser( + "start", + help="Launch the Weights Studio UI (bare `start`), or `start example` to run a demo") + _add_launch_flags(start_parser, with_certs_dir=False) start_sub = start_parser.add_subparsers(dest="start_target") example_parser = start_sub.add_parser( "example", help="Start a bundled PyTorch example (default: classification)") @@ -1238,16 +1291,22 @@ def main(): tunnel_connect(args) elif args.command == "se": ui_secure_environment(args) + elif args.command == "launch": + # Primary UI-launch command. + ui_launch(args) elif args.command == "ui": + # Legacy: `weightslab ui launch` still works (prefer `weightslab launch`). if getattr(args, "action", None) == "launch": ui_launch(args) else: ui_parser.print_help() elif args.command == "start": + # `weightslab start example ...` runs a demo; bare `weightslab start` + # launches the UI (alias of `weightslab launch`). if getattr(args, "start_target", None) == "example": example_start(args) else: - start_parser.print_help() + ui_launch(args) elif args.command == "example": # Alias for `start example` β€” tolerate the swapped subcommand order # (`weightslab example start [flags]`) and the bare `weightslab example`.