litert-cli 0.1.0__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.
Files changed (67) hide show
  1. examples/litert_cli.ipynb +313 -0
  2. examples/models/presets/default.py +19 -0
  3. examples/run_cli_demo.sh +38 -0
  4. examples/run_cli_npu.sh +89 -0
  5. examples/run_commands.sh +67 -0
  6. examples/run_models.sh +63 -0
  7. examples/run_smoke_tests.sh +58 -0
  8. examples/utils.ps1 +163 -0
  9. examples/utils.sh +184 -0
  10. litert_cli/__init__.py +15 -0
  11. litert_cli/commands/benchmark/__init__.py +16 -0
  12. litert_cli/commands/benchmark/android.py +212 -0
  13. litert_cli/commands/benchmark/cli.py +294 -0
  14. litert_cli/commands/benchmark/desktop.py +228 -0
  15. litert_cli/commands/benchmark/gcp.py +336 -0
  16. litert_cli/commands/clean.py +73 -0
  17. litert_cli/commands/compile.py +211 -0
  18. litert_cli/commands/convert/__init__.py +20 -0
  19. litert_cli/commands/convert/cli.py +255 -0
  20. litert_cli/commands/convert/generic.py +211 -0
  21. litert_cli/commands/convert/huggingface.py +175 -0
  22. litert_cli/commands/delete.py +56 -0
  23. litert_cli/commands/download.py +274 -0
  24. litert_cli/commands/import.py +124 -0
  25. litert_cli/commands/list.py +132 -0
  26. litert_cli/commands/lm.py +74 -0
  27. litert_cli/commands/quantize.py +193 -0
  28. litert_cli/commands/run/__init__.py +16 -0
  29. litert_cli/commands/run/android.py +394 -0
  30. litert_cli/commands/run/cli.py +297 -0
  31. litert_cli/commands/run/desktop.py +340 -0
  32. litert_cli/commands/visualize.py +234 -0
  33. litert_cli/core/android_utils.py +304 -0
  34. litert_cli/core/android_utils_test.py +236 -0
  35. litert_cli/core/constants.py +131 -0
  36. litert_cli/core/deps.py +180 -0
  37. litert_cli/core/deps_test.py +101 -0
  38. litert_cli/core/inputs.py +203 -0
  39. litert_cli/core/inputs_test.py +176 -0
  40. litert_cli/core/log_filters.py +50 -0
  41. litert_cli/core/models.py +96 -0
  42. litert_cli/core/npu_utils.py +382 -0
  43. litert_cli/core/targets_manager.py +192 -0
  44. litert_cli/core/utils.py +58 -0
  45. litert_cli/litert.py +119 -0
  46. litert_cli/litert_help_test.py +51 -0
  47. litert_cli/litert_test.py +88 -0
  48. litert_cli/models/__init__.py +145 -0
  49. litert_cli/models/asr/__init__.py +15 -0
  50. litert_cli/models/asr/asr_model.py +108 -0
  51. litert_cli/models/asr/parakeet_ctc.py +165 -0
  52. litert_cli/models/asr/runner.py +482 -0
  53. litert_cli/models/base.py +57 -0
  54. litert_cli/test_data/dummy_calib_data.py +26 -0
  55. litert_cli/test_data/dummy_cv_model.py +52 -0
  56. litert_cli/test_data/dummy_cv_model.tflite +0 -0
  57. litert_cli/test_data/generate_test_inputs.py +51 -0
  58. litert_cli/test_data/mobilenet_v3_calib_data.py +25 -0
  59. litert_cli/test_data/quantize_recipe.json +16 -0
  60. litert_cli/test_data/resnet18.py +31 -0
  61. litert_cli-0.1.0.dist-info/METADATA +38 -0
  62. litert_cli-0.1.0.dist-info/RECORD +67 -0
  63. litert_cli-0.1.0.dist-info/WHEEL +5 -0
  64. litert_cli-0.1.0.dist-info/entry_points.txt +2 -0
  65. litert_cli-0.1.0.dist-info/licenses/LICENSE +202 -0
  66. litert_cli-0.1.0.dist-info/top_level.txt +3 -0
  67. tools/build_wheels.py +122 -0
@@ -0,0 +1,234 @@
1
+ # Copyright 2026 The LiteRT CLI Authors.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+
16
+ """Command line interface for visualizing LiteRT models.
17
+
18
+ This module provides the `litert visualize` command, which launches the
19
+ Model Explorer web application to inspect the architecture of TFLite models.
20
+
21
+ Key Features:
22
+ - Background Execution: The web server runs entirely in the background,
23
+ instantly returning control of the terminal to the user.
24
+ - URL Printing: The command calculates and prints a click-able URL directly
25
+ in the terminal, bypassing IDE port-forwarding interceptions that often
26
+ strip URL query parameters.
27
+ - Server Reuse: By default, multiple visualizations reuse the same server
28
+ instance, preventing port exhaustion and reducing memory footprint.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import json
34
+ import pathlib
35
+ import socket
36
+ import subprocess
37
+ import sys
38
+ import textwrap
39
+ import urllib.parse
40
+
41
+ import click
42
+
43
+ from ..core import deps
44
+
45
+
46
+ def _is_port_in_use(port_num: int) -> bool:
47
+ """Checks if a port is in use on localhost.
48
+
49
+ Args:
50
+ port_num: Port to check.
51
+
52
+ Returns:
53
+ True if the port is bound and in use, False otherwise.
54
+ """
55
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
56
+ return s.connect_ex(('localhost', port_num)) == 0
57
+
58
+
59
+ def _find_available_port(start_port: int, max_attempts: int = 20) -> int:
60
+ """Finds the first available port on localhost starting from start_port.
61
+
62
+ Args:
63
+ start_port: Port to start searching from.
64
+ max_attempts: Maximum number of ports to check.
65
+
66
+ Returns:
67
+ The first available port found.
68
+ """
69
+ return next(
70
+ (
71
+ p
72
+ for p in range(start_port, start_port + max_attempts)
73
+ if not _is_port_in_use(p)
74
+ ),
75
+ start_port,
76
+ )
77
+
78
+
79
+ @click.command(
80
+ 'visualize',
81
+ help=textwrap.dedent("""\
82
+ Runs model explorer to visualize the model architecture.
83
+
84
+ MODEL_PATH: Path to the LiteRT model (.tflite) to visualize.
85
+
86
+ Examples:
87
+
88
+ 1. Visualize a single model (starts a new server if none exists):
89
+
90
+ $ litert visualize /path/to/model.tflite
91
+
92
+ 2. Visualize a different model (reuses existing server, refreshes browser):
93
+
94
+ $ litert visualize /path/to/another_model.tflite
95
+
96
+ 3. Compare two models side-by-side (forces a new server on a new port):
97
+
98
+ $ litert visualize /path/to/model_A.tflite
99
+ $ litert visualize /path/to/model_B.tflite --no-reuse-server
100
+
101
+ 4. Clean up and stop all running Model Explorer servers:
102
+
103
+ $ litert visualize --stop-all
104
+ """),
105
+ )
106
+ @click.argument(
107
+ 'model_path',
108
+ type=str,
109
+ required=False,
110
+ )
111
+ @click.option(
112
+ '--reuse-server/--no-reuse-server',
113
+ default=True,
114
+ help='Reuse running Model Explorer server.',
115
+ )
116
+ @click.option(
117
+ '--stop-all',
118
+ is_flag=True,
119
+ help='Stop all running Model Explorer servers.',
120
+ )
121
+ @deps.require_extra('visualize')
122
+ def visualize_cmd(
123
+ model_path: str | None, *, reuse_server: bool, stop_all: bool
124
+ ) -> None:
125
+ """Runs model explorer to visualize the model architecture.
126
+
127
+ Args:
128
+ model_path: Path to the LiteRT model (.tflite) or Model Reference to
129
+ visualize.
130
+ reuse_server: Whether to reuse an already running Model Explorer server.
131
+ stop_all: If True, stops all running Model Explorer servers instead.
132
+ """
133
+ from litert_cli.core import models as core_models
134
+
135
+ if stop_all:
136
+ click.echo('Attempting to stop all running Model Explorer servers...')
137
+ try:
138
+ # Use pkill to find and terminate all model-explorer processes
139
+ subprocess.run(
140
+ ['pkill', '-f', 'model-explorer'],
141
+ check=True,
142
+ stdout=subprocess.DEVNULL,
143
+ stderr=subprocess.DEVNULL,
144
+ )
145
+ click.secho(
146
+ 'Successfully stopped all Model Explorer servers.',
147
+ fg='green',
148
+ )
149
+ except subprocess.CalledProcessError:
150
+ click.secho('No running Model Explorer servers found.', fg='yellow')
151
+ return
152
+
153
+ if not model_path:
154
+ raise click.UsageError('Missing argument "MODEL_PATH".')
155
+
156
+ resolved_model_path, _ = core_models.resolve_model_reference(model_path)
157
+ if str(resolved_model_path) != str(model_path):
158
+ click.echo(f"Resolved model '{model_path}' to '{resolved_model_path}'")
159
+
160
+ resolved_model_path = pathlib.Path(resolved_model_path).resolve()
161
+
162
+ click.echo(
163
+ f'Starting Model Explorer visualization for {resolved_model_path} in the'
164
+ ' background...'
165
+ )
166
+ # Check for available port so we can print the exact URL
167
+ port = (
168
+ 8081
169
+ if reuse_server and _is_port_in_use(8081)
170
+ else _find_available_port(8081)
171
+ )
172
+
173
+ # Build the exact model explorer data URL
174
+ data = {'models': [{'url': str(resolved_model_path)}]}
175
+ data_param = urllib.parse.quote(
176
+ json.dumps(data, separators=(',', ':')), safe=':,'
177
+ )
178
+ url = f'http://localhost:{port}/?data={data_param}'
179
+
180
+ model_explorer_bin = None
181
+ if sys.executable:
182
+ python_dir = pathlib.Path(sys.executable).parent
183
+ # Assume model_explorer binary is in the same directory as the python
184
+ model_explorer_bin = python_dir / 'model-explorer'
185
+
186
+ cmd = [
187
+ str(model_explorer_bin)
188
+ if model_explorer_bin and model_explorer_bin.exists()
189
+ else 'model-explorer',
190
+ str(resolved_model_path),
191
+ '--no_open_in_browser',
192
+ f'--port={port}',
193
+ ]
194
+ if reuse_server:
195
+ cmd.append('--reuse_server')
196
+ cmd.append(f'--reuse_server_port={port}')
197
+
198
+ # Launch as a fully detached daemon so the terminal isn't blocked.
199
+ try:
200
+ subprocess.Popen(
201
+ cmd,
202
+ stdout=subprocess.DEVNULL,
203
+ stderr=subprocess.DEVNULL,
204
+ start_new_session=True,
205
+ )
206
+
207
+ click.secho(
208
+ '\nModel Explorer server is running in the background.', fg='green'
209
+ )
210
+ click.secho(
211
+ '► Please click the link below to view your model:',
212
+ fg='cyan',
213
+ bold=True,
214
+ )
215
+ click.secho(f'\n {url}\n')
216
+ if reuse_server:
217
+ click.secho(
218
+ 'It will reuse the existing server instance if one is already'
219
+ ' running, avoiding port conflicts.',
220
+ fg='cyan',
221
+ )
222
+ except FileNotFoundError:
223
+ click.secho(
224
+ '\nError: "model-explorer" command not found.', fg='red', bold=True
225
+ )
226
+ click.secho(
227
+ 'Please make sure Model Explorer is installed and available in your'
228
+ ' PATH.',
229
+ fg='yellow',
230
+ )
231
+ click.secho(
232
+ 'You can install it via: pip install model-explorer',
233
+ fg='cyan',
234
+ )
@@ -0,0 +1,304 @@
1
+ # Copyright 2026 The LiteRT CLI Authors.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+
16
+ """Android utility functions for LiteRT CLI."""
17
+
18
+ from __future__ import annotations
19
+
20
+ from collections.abc import Sequence
21
+ import pathlib
22
+ import subprocess
23
+
24
+ import click
25
+ import requests
26
+
27
+ from litert_cli.core import constants
28
+
29
+
30
+ class DownloadError(Exception):
31
+ """A file download failed."""
32
+
33
+ pass
34
+
35
+
36
+ def _construct_adb_command(command: Sequence[str], device_id: str | None) -> list[str]:
37
+ """Constructs an adb command list, including the device ID if provided."""
38
+ if device_id:
39
+ return ["adb", "-s", device_id, *command]
40
+ return ["adb", *command]
41
+
42
+
43
+ def check_adb(device_id: str | None = None) -> None:
44
+ """Checks if adb device is available.
45
+
46
+ Verifies that exactly one authorized device is connected and responding to
47
+ commands.
48
+
49
+ Args:
50
+ device_id: Optional. The serial number of the target device.
51
+
52
+ Raises:
53
+ click.ClickException: If adb is missing or device is unauthorized/offline.
54
+ """
55
+ try:
56
+ subprocess.run(
57
+ _construct_adb_command(["get-state"], device_id),
58
+ check=True,
59
+ stdout=subprocess.DEVNULL,
60
+ stderr=subprocess.DEVNULL,
61
+ )
62
+ except FileNotFoundError as e:
63
+ raise click.ClickException(
64
+ "adb command not found. Please ensure Android platform-tools is in"
65
+ " your PATH."
66
+ ) from e
67
+ except subprocess.CalledProcessError as e:
68
+ device_info = f" for device {device_id}" if device_id else ""
69
+ raise click.ClickException(
70
+ f"No Android device found{device_info} or it is not authorized. Try"
71
+ " running 'adb devices' to check."
72
+ ) from e
73
+
74
+
75
+ def get_android_abi(device_id: str | None = None) -> str:
76
+ """Gets the CPU ABI of the connected Android device via adb.
77
+
78
+ Args:
79
+ device_id: Optional. The serial number of the target device.
80
+
81
+ Returns:
82
+ The CPU ABI string (e.g., 'arm64-v8a').
83
+
84
+ Raises:
85
+ click.ClickException: If querying Android ABI fails or adb is missing.
86
+ """
87
+ try:
88
+ return subprocess.run(
89
+ _construct_adb_command(
90
+ ["shell", "getprop", "ro.product.cpu.abi"], device_id
91
+ ),
92
+ capture_output=True,
93
+ text=True,
94
+ check=True,
95
+ ).stdout.strip()
96
+ except subprocess.CalledProcessError as e:
97
+ device_info = f" for device {device_id}" if device_id else ""
98
+ raise click.ClickException(f"Error querying Android ABI{device_info}") from e
99
+
100
+
101
+ def _ensure_downloaded_file(
102
+ abi: str,
103
+ file_name: str,
104
+ base_url: str,
105
+ *,
106
+ make_executable: bool = False,
107
+ ) -> pathlib.Path:
108
+ """Downloads a file for the given ABI if not cached.
109
+
110
+ Args:
111
+ abi: The Android CPU ABI (e.g., 'arm64-v8a').
112
+ file_name: The file name to download.
113
+ base_url: The base URL to download from.
114
+ make_executable: Whether to make the file executable.
115
+
116
+ Returns:
117
+ The absolute local path to the cached file.
118
+
119
+ Raises:
120
+ click.ClickException: If the architecture is not supported.
121
+ DownloadError: If the download or file operations fail.
122
+ """
123
+ if "arm64" not in abi:
124
+ raise click.ClickException(
125
+ f"Architecture {abi!r} is not supported for automatic downloading of"
126
+ f" {file_name!r}. Only arm64 is supported for these specific binaries."
127
+ )
128
+
129
+ download_url = f"{base_url}/{file_name}"
130
+ cache_dir = pathlib.Path(constants.LITERT_CLI_CACHE_DIR) / "binaries" / abi
131
+ cache_dir.mkdir(parents=True, exist_ok=True)
132
+
133
+ cached_file_path = cache_dir / file_name
134
+ if cached_file_path.exists():
135
+ return cached_file_path
136
+
137
+ click.secho(f"Downloading {file_name!r} for {abi!r}...", fg="cyan")
138
+ tmp_cached_file = cached_file_path.with_suffix(".tmp")
139
+ try:
140
+ with requests.get(download_url, stream=True, timeout=10) as response:
141
+ response.raise_for_status()
142
+ content_length = response.headers.get("Content-Length")
143
+ total_size = int(content_length) if content_length else 0
144
+
145
+ bar_length = total_size if total_size > 0 else None
146
+ bar_label = f"Downloading {file_name}"
147
+ if bar_length is None:
148
+ click.secho(
149
+ f"Content-Length header not found for {file_name!r}, using"
150
+ " indeterminate progress bar.",
151
+ fg="yellow",
152
+ )
153
+ bar_label += " (size unknown)"
154
+
155
+ with click.progressbar(
156
+ length=bar_length, label=bar_label
157
+ ) as bar:
158
+ with open(tmp_cached_file, "wb") as f:
159
+ for buffer in response.iter_content(chunk_size=8192):
160
+ f.write(buffer)
161
+ bar.update(len(buffer))
162
+
163
+ tmp_cached_file.rename(cached_file_path)
164
+ if make_executable:
165
+ cached_file_path.chmod(0o755)
166
+ return cached_file_path
167
+ except (requests.exceptions.RequestException, OSError) as e:
168
+ if cached_file_path.exists():
169
+ cached_file_path.unlink()
170
+ if tmp_cached_file.exists():
171
+ tmp_cached_file.unlink()
172
+ raise DownloadError(
173
+ f"Failed to download {file_name!r} from {download_url!r}"
174
+ ) from e
175
+
176
+
177
+ def _ensure_downloaded_binary(abi: str, tool_name: str) -> pathlib.Path:
178
+ """Downloads the pre-built binary for the given ABI if not cached."""
179
+ return _ensure_downloaded_file(
180
+ abi,
181
+ tool_name,
182
+ constants.LITERT_BINARIES_BASE_URL_ANDROID,
183
+ make_executable=True,
184
+ )
185
+
186
+
187
+ def find_android_binary(tool_name: str, abi: str) -> pathlib.Path:
188
+ """Locates or downloads an Android executable binary.
189
+
190
+ Always downloads from a fixed URL (cached locally).
191
+
192
+ Args:
193
+ tool_name: Binary name (e.g. 'run_model').
194
+ abi: Target Android CPU ABI.
195
+
196
+ Returns:
197
+ The absolute path to the binary.
198
+
199
+ Raises:
200
+ click.ClickException: If the binary could not be found or downloaded.
201
+ """
202
+ try:
203
+ downloaded_bin = _ensure_downloaded_binary(abi, tool_name)
204
+ if downloaded_bin.exists():
205
+ return downloaded_bin
206
+ else:
207
+ raise click.ClickException(
208
+ f"Downloaded binary {tool_name!r} for ABI {abi!r} does not exist."
209
+ )
210
+ except DownloadError as e:
211
+ raise click.ClickException(
212
+ f"Could not find or download {tool_name!r} for ABI {abi!r}: {e}"
213
+ ) from e
214
+
215
+
216
+ def _ensure_downloaded_library(
217
+ abi: str,
218
+ lib_name: str,
219
+ base_url: str = constants.LITERT_BINARIES_BASE_URL_ANDROID,
220
+ ) -> pathlib.Path:
221
+ """Downloads the pre-built library for the given ABI if not cached."""
222
+ return _ensure_downloaded_file(abi, lib_name, base_url, make_executable=False)
223
+
224
+
225
+ def find_android_lib(
226
+ lib_name: str,
227
+ abi: str,
228
+ base_url: str = constants.LITERT_BINARIES_BASE_URL_ANDROID,
229
+ ) -> pathlib.Path:
230
+ """Locates or downloads an Android library.
231
+
232
+ Always downloads from a fixed URL (cached locally).
233
+
234
+ Args:
235
+ lib_name: library name (e.g. 'libLiteRt.so').
236
+ abi: Target Android CPU ABI.
237
+ base_url: The base URL to download the library from.
238
+
239
+ Returns:
240
+ The absolute path to the library.
241
+
242
+ Raises:
243
+ click.ClickException: If the library could not be found or downloaded.
244
+ """
245
+ try:
246
+ downloaded_bin = _ensure_downloaded_library(abi, lib_name, base_url)
247
+ if downloaded_bin.exists():
248
+ return downloaded_bin
249
+ else:
250
+ raise click.ClickException(
251
+ f"Downloaded library {lib_name!r} for ABI {abi!r} does not exist."
252
+ )
253
+ except DownloadError as e:
254
+ raise click.ClickException(
255
+ f"Could not find or download {lib_name!r} for ABI {abi!r}: {e}"
256
+ ) from e
257
+
258
+
259
+ def find_npu_dispatch_lib(soc_vendor: str, abi: str) -> pathlib.Path:
260
+ """Finds and downloads the NPU dispatch library for the given SoC vendor.
261
+
262
+ Args:
263
+ soc_vendor: The NPU vendor ("qualcomm" or "mediatek").
264
+ abi: Target Android CPU ABI.
265
+
266
+ Returns:
267
+ The absolute path to the local downloaded backend dispatch library.
268
+ """
269
+ if soc_vendor == "qualcomm":
270
+ lib_name = "libLiteRtDispatch_Qualcomm.so"
271
+ elif soc_vendor == "mediatek":
272
+ lib_name = "libLiteRtDispatch_MediaTek.so"
273
+ else:
274
+ raise click.ClickException(
275
+ f"Unsupported NPU vendor for dispatch: {soc_vendor!r}"
276
+ )
277
+
278
+ return find_android_lib(
279
+ lib_name, abi, base_url=constants.LITERT_BINARIES_BASE_URL_ANDROID
280
+ )
281
+
282
+
283
+ def find_npu_compiler_plugin_lib(soc_vendor: str, abi: str) -> pathlib.Path:
284
+ """Finds and downloads the NPU compiler plugin library for the given SoC vendor.
285
+
286
+ Args:
287
+ soc_vendor: The NPU vendor ("qualcomm" or "mediatek").
288
+ abi: Target Android CPU ABI.
289
+
290
+ Returns:
291
+ The absolute path to the local downloaded backend compiler plugin library.
292
+ """
293
+ if soc_vendor == "qualcomm":
294
+ lib_name = "libLiteRtCompilerPlugin_Qualcomm.so"
295
+ elif soc_vendor == "mediatek":
296
+ lib_name = "libLiteRtCompilerPlugin_MediaTek.so"
297
+ else:
298
+ raise click.ClickException(
299
+ f"Unsupported NPU vendor for compiler plugin: {soc_vendor!r}"
300
+ )
301
+
302
+ return find_android_lib(
303
+ lib_name, abi, base_url=constants.LITERT_BINARIES_BASE_URL_ANDROID
304
+ )