qualia-cli 0.1.0__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.
- qualia_cli-0.1.0/.gitignore +84 -0
- qualia_cli-0.1.0/PKG-INFO +79 -0
- qualia_cli-0.1.0/README.md +56 -0
- qualia_cli-0.1.0/pyproject.toml +81 -0
- qualia_cli-0.1.0/src/qualia_cli/__init__.py +3 -0
- qualia_cli-0.1.0/src/qualia_cli/client.py +29 -0
- qualia_cli-0.1.0/src/qualia_cli/commands/__init__.py +0 -0
- qualia_cli-0.1.0/src/qualia_cli/commands/auth.py +43 -0
- qualia_cli-0.1.0/src/qualia_cli/commands/credits.py +21 -0
- qualia_cli-0.1.0/src/qualia_cli/commands/datasets.py +85 -0
- qualia_cli-0.1.0/src/qualia_cli/commands/finetune.py +200 -0
- qualia_cli-0.1.0/src/qualia_cli/commands/instances.py +42 -0
- qualia_cli-0.1.0/src/qualia_cli/commands/models.py +95 -0
- qualia_cli-0.1.0/src/qualia_cli/commands/projects.py +82 -0
- qualia_cli-0.1.0/src/qualia_cli/config.py +63 -0
- qualia_cli-0.1.0/src/qualia_cli/main.py +76 -0
- qualia_cli-0.1.0/src/qualia_cli/output.py +42 -0
- qualia_cli-0.1.0/tests/__init__.py +0 -0
- qualia_cli-0.1.0/uv.lock +822 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# qualia
|
|
2
|
+
qcore.config.toml
|
|
3
|
+
.qdev/*
|
|
4
|
+
.env
|
|
5
|
+
|
|
6
|
+
# python-generated
|
|
7
|
+
__pycache__/
|
|
8
|
+
*.py[oc]
|
|
9
|
+
build/
|
|
10
|
+
dist/
|
|
11
|
+
wheels/
|
|
12
|
+
*.egg-info
|
|
13
|
+
.coverage
|
|
14
|
+
.coverage.*
|
|
15
|
+
htmlcov/
|
|
16
|
+
|
|
17
|
+
# virtual envs
|
|
18
|
+
.venv
|
|
19
|
+
|
|
20
|
+
# ruff stuff:
|
|
21
|
+
.ruff_cache/
|
|
22
|
+
|
|
23
|
+
# mac
|
|
24
|
+
.DS_Store
|
|
25
|
+
|
|
26
|
+
# Jupyter Notebook
|
|
27
|
+
.ipynb_checkpoints
|
|
28
|
+
.ipython
|
|
29
|
+
|
|
30
|
+
# Terraform
|
|
31
|
+
*/*/.terraform/
|
|
32
|
+
*.tfstate
|
|
33
|
+
*.tfstate.backup
|
|
34
|
+
*.tfplan
|
|
35
|
+
|
|
36
|
+
# supabase
|
|
37
|
+
.temp
|
|
38
|
+
reset.sql
|
|
39
|
+
|
|
40
|
+
# generated docs
|
|
41
|
+
ui/docs/src/assets/openapi.json
|
|
42
|
+
|
|
43
|
+
# nix
|
|
44
|
+
.direnv
|
|
45
|
+
result
|
|
46
|
+
|
|
47
|
+
# playwright-cli
|
|
48
|
+
.playwright-cli/
|
|
49
|
+
|
|
50
|
+
# Windows
|
|
51
|
+
nul
|
|
52
|
+
lerobot
|
|
53
|
+
|
|
54
|
+
# Windows
|
|
55
|
+
nul
|
|
56
|
+
|
|
57
|
+
# qcore runtime files
|
|
58
|
+
qcore.log
|
|
59
|
+
qcore.pid
|
|
60
|
+
|
|
61
|
+
# Dev credential files (auto-generated by setup workflow)
|
|
62
|
+
dev_project_id.txt
|
|
63
|
+
dev_user_id.txt
|
|
64
|
+
dev_access_token.txt
|
|
65
|
+
.env
|
|
66
|
+
|
|
67
|
+
# Claude Code sandbox artifacts (https://github.com/anthropics/claude-code/issues/17727)
|
|
68
|
+
.claude/agents
|
|
69
|
+
.claude/commands
|
|
70
|
+
.bash_profile
|
|
71
|
+
.bashrc
|
|
72
|
+
.gitconfig
|
|
73
|
+
.gitmodules
|
|
74
|
+
.idea
|
|
75
|
+
.mcp.json
|
|
76
|
+
.profile
|
|
77
|
+
.ripgreprc
|
|
78
|
+
.zprofile
|
|
79
|
+
.zshrc
|
|
80
|
+
/HEAD
|
|
81
|
+
/config
|
|
82
|
+
/hooks
|
|
83
|
+
/objects
|
|
84
|
+
/refs
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: qualia-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI for the Qualia Studios VLA fine-tuning platform
|
|
5
|
+
Project-URL: Homepage, https://qualiastudios.dev
|
|
6
|
+
Project-URL: Documentation, https://docs.qualiastudios.dev
|
|
7
|
+
Author: Qualia Studios
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Keywords: cli,fine-tuning,qualia,robotics,vla
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Requires-Dist: qualia-sdk>=0.2.0
|
|
20
|
+
Requires-Dist: rich>=13.0.0
|
|
21
|
+
Requires-Dist: typer>=0.15.0
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# Qualia CLI
|
|
25
|
+
|
|
26
|
+
Terminal interface for the [Qualia](https://qualiastudios.dev) VLA fine-tuning platform.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
pip install qualia-cli
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Or with [uv](https://docs.astral.sh/uv/):
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
uv tool install qualia-cli
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quick start
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
# Authenticate (pick one)
|
|
44
|
+
qualia auth login # interactive, saves to config
|
|
45
|
+
export QUALIA_API_KEY="your-api-key" # env var
|
|
46
|
+
qualia --token <KEY> <command> # inline, for CI/scripts
|
|
47
|
+
|
|
48
|
+
# List available models
|
|
49
|
+
qualia models list
|
|
50
|
+
|
|
51
|
+
# List GPU instances
|
|
52
|
+
qualia instances list
|
|
53
|
+
|
|
54
|
+
# Create a project
|
|
55
|
+
qualia projects create "My Robot"
|
|
56
|
+
|
|
57
|
+
# Start a fine-tuning job
|
|
58
|
+
qualia finetune create \
|
|
59
|
+
--project-id <id> \
|
|
60
|
+
--vla-type smolvla \
|
|
61
|
+
--model-id lerobot/smolvla_base \
|
|
62
|
+
--dataset-id lerobot/pusht \
|
|
63
|
+
--dataset-last-modified "2025-01-15T10:00:00Z" \
|
|
64
|
+
--model-last-modified "2025-01-15T10:00:00Z" \
|
|
65
|
+
--hours 2.0 \
|
|
66
|
+
--camera-mappings '{"cam_1": "observation.images.top"}'
|
|
67
|
+
|
|
68
|
+
# Check job status
|
|
69
|
+
qualia finetune get <job-id>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## JSON output
|
|
73
|
+
|
|
74
|
+
All commands support `--json` for machine-readable output:
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
qualia projects list --json
|
|
78
|
+
qualia credits --json
|
|
79
|
+
```
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Qualia CLI
|
|
2
|
+
|
|
3
|
+
Terminal interface for the [Qualia](https://qualiastudios.dev) VLA fine-tuning platform.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pip install qualia-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or with [uv](https://docs.astral.sh/uv/):
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
uv tool install qualia-cli
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
# Authenticate (pick one)
|
|
21
|
+
qualia auth login # interactive, saves to config
|
|
22
|
+
export QUALIA_API_KEY="your-api-key" # env var
|
|
23
|
+
qualia --token <KEY> <command> # inline, for CI/scripts
|
|
24
|
+
|
|
25
|
+
# List available models
|
|
26
|
+
qualia models list
|
|
27
|
+
|
|
28
|
+
# List GPU instances
|
|
29
|
+
qualia instances list
|
|
30
|
+
|
|
31
|
+
# Create a project
|
|
32
|
+
qualia projects create "My Robot"
|
|
33
|
+
|
|
34
|
+
# Start a fine-tuning job
|
|
35
|
+
qualia finetune create \
|
|
36
|
+
--project-id <id> \
|
|
37
|
+
--vla-type smolvla \
|
|
38
|
+
--model-id lerobot/smolvla_base \
|
|
39
|
+
--dataset-id lerobot/pusht \
|
|
40
|
+
--dataset-last-modified "2025-01-15T10:00:00Z" \
|
|
41
|
+
--model-last-modified "2025-01-15T10:00:00Z" \
|
|
42
|
+
--hours 2.0 \
|
|
43
|
+
--camera-mappings '{"cam_1": "observation.images.top"}'
|
|
44
|
+
|
|
45
|
+
# Check job status
|
|
46
|
+
qualia finetune get <job-id>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## JSON output
|
|
50
|
+
|
|
51
|
+
All commands support `--json` for machine-readable output:
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
qualia projects list --json
|
|
55
|
+
qualia credits --json
|
|
56
|
+
```
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "qualia-cli"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "CLI for the Qualia Studios VLA fine-tuning platform"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
requires-python = ">=3.10"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Qualia Studios" },
|
|
10
|
+
]
|
|
11
|
+
keywords = ["qualia", "vla", "robotics", "fine-tuning", "cli"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.10",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Typing :: Typed",
|
|
21
|
+
]
|
|
22
|
+
dependencies = [
|
|
23
|
+
"qualia-sdk>=0.2.0",
|
|
24
|
+
"typer>=0.15.0",
|
|
25
|
+
"rich>=13.0.0",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://qualiastudios.dev"
|
|
30
|
+
Documentation = "https://docs.qualiastudios.dev"
|
|
31
|
+
|
|
32
|
+
[project.scripts]
|
|
33
|
+
qualia = "qualia_cli.main:cli"
|
|
34
|
+
|
|
35
|
+
[dependency-groups]
|
|
36
|
+
test = [
|
|
37
|
+
"pytest>=8.0.0",
|
|
38
|
+
"pytest-cov>=4.0.0",
|
|
39
|
+
]
|
|
40
|
+
dev = [
|
|
41
|
+
"ruff==0.15.1",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[tool.uv]
|
|
45
|
+
default-groups = [
|
|
46
|
+
"test",
|
|
47
|
+
"dev",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[tool.uv.sources]
|
|
51
|
+
qualia-sdk = { path = "../python", editable = true }
|
|
52
|
+
|
|
53
|
+
[build-system]
|
|
54
|
+
requires = ["hatchling"]
|
|
55
|
+
build-backend = "hatchling.build"
|
|
56
|
+
|
|
57
|
+
[tool.hatch.build.targets.wheel]
|
|
58
|
+
packages = ["src/qualia_cli"]
|
|
59
|
+
|
|
60
|
+
[tool.ruff]
|
|
61
|
+
line-length = 89
|
|
62
|
+
target-version = "py310"
|
|
63
|
+
|
|
64
|
+
[tool.ruff.lint]
|
|
65
|
+
select = [
|
|
66
|
+
"I", # isort
|
|
67
|
+
"F", # pyflakes
|
|
68
|
+
"E", # pycodestyle errors
|
|
69
|
+
"W", # pycodestyle warnings
|
|
70
|
+
"B", # flake8-bugbear
|
|
71
|
+
"UP", # pyupgrade
|
|
72
|
+
"SIM", # flake8-simplify
|
|
73
|
+
]
|
|
74
|
+
ignore = [
|
|
75
|
+
"E501", # line too long
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
[tool.pytest.ini_options]
|
|
79
|
+
testpaths = ["tests"]
|
|
80
|
+
python_files = ["test_*.py"]
|
|
81
|
+
addopts = "-v --tb=short"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Shared client factory."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from qualia import AuthenticationError, Qualia
|
|
6
|
+
|
|
7
|
+
from qualia_cli.config import load_config
|
|
8
|
+
from qualia_cli.output import print_error
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_client() -> Qualia:
|
|
12
|
+
from qualia_cli.main import _base_url_override, _token_override
|
|
13
|
+
|
|
14
|
+
cfg = load_config()
|
|
15
|
+
kwargs: dict = {}
|
|
16
|
+
if _token_override:
|
|
17
|
+
kwargs["api_key"] = _token_override
|
|
18
|
+
elif cfg.api_key:
|
|
19
|
+
kwargs["api_key"] = cfg.api_key
|
|
20
|
+
if _base_url_override:
|
|
21
|
+
kwargs["base_url"] = _base_url_override
|
|
22
|
+
elif cfg.base_url:
|
|
23
|
+
kwargs["base_url"] = cfg.base_url
|
|
24
|
+
try:
|
|
25
|
+
return Qualia(**kwargs)
|
|
26
|
+
except AuthenticationError as e:
|
|
27
|
+
print_error(str(e))
|
|
28
|
+
print_error("Set QUALIA_API_KEY or run: qualia auth login --token <KEY>")
|
|
29
|
+
raise SystemExit(1) from e
|
|
File without changes
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Auth commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from qualia_cli.config import load_config, save_config
|
|
8
|
+
from qualia_cli.output import print_error
|
|
9
|
+
|
|
10
|
+
app = typer.Typer(help="Authentication.")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@app.command("login")
|
|
14
|
+
def login(
|
|
15
|
+
api_key: str = typer.Option(
|
|
16
|
+
..., prompt="API key", hide_input=True, help="Your Qualia API key"
|
|
17
|
+
),
|
|
18
|
+
base_url: str | None = typer.Option(
|
|
19
|
+
None, help="API base URL (e.g. http://localhost:8000)"
|
|
20
|
+
),
|
|
21
|
+
) -> None:
|
|
22
|
+
"""Save your API key (and optional base URL) for future use."""
|
|
23
|
+
try:
|
|
24
|
+
path = save_config(api_key, base_url)
|
|
25
|
+
typer.echo(f"Config saved to {path}")
|
|
26
|
+
except OSError as e:
|
|
27
|
+
print_error(f"Could not write config: {e}")
|
|
28
|
+
raise SystemExit(1) from e
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@app.command("status")
|
|
32
|
+
def status() -> None:
|
|
33
|
+
"""Check current authentication config."""
|
|
34
|
+
cfg = load_config()
|
|
35
|
+
if cfg.api_key:
|
|
36
|
+
typer.echo(f"API key: {cfg.api_key[:8]}...")
|
|
37
|
+
else:
|
|
38
|
+
typer.echo("Not authenticated. Run: qualia auth login")
|
|
39
|
+
raise SystemExit(1)
|
|
40
|
+
if cfg.base_url:
|
|
41
|
+
typer.echo(f"Base URL: {cfg.base_url}")
|
|
42
|
+
else:
|
|
43
|
+
typer.echo("Base URL: https://api.qualiastudios.dev (default)")
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Credits commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from qualia_cli.client import get_client
|
|
8
|
+
from qualia_cli.output import print_json
|
|
9
|
+
|
|
10
|
+
app = typer.Typer(help="View credit balance.")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@app.callback(invoke_without_command=True)
|
|
14
|
+
def credits(json: bool = typer.Option(False, "--json", help="JSON output")) -> None:
|
|
15
|
+
"""Show your current credit balance."""
|
|
16
|
+
client = get_client()
|
|
17
|
+
balance = client.credits.get()
|
|
18
|
+
if json:
|
|
19
|
+
print_json(balance)
|
|
20
|
+
else:
|
|
21
|
+
typer.echo(f"Credits: {balance.balance}")
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Datasets commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from uuid import UUID
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from qualia_cli.client import get_client
|
|
10
|
+
from qualia_cli.output import print_json, print_table
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(help="Dataset utilities.")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@app.command("list")
|
|
16
|
+
def list_datasets(
|
|
17
|
+
limit: int = typer.Option(20, "--limit", "-n", help="Page size (1-100)"),
|
|
18
|
+
cursor: str = typer.Option(None, "--cursor", help="Pagination cursor"),
|
|
19
|
+
sort: str = typer.Option(
|
|
20
|
+
"newest", "--sort", "-s", help="newest|oldest|updated|name"
|
|
21
|
+
),
|
|
22
|
+
search: str = typer.Option(None, "--search", "-q", help="Search dataset path"),
|
|
23
|
+
subtype: str = typer.Option(
|
|
24
|
+
None, "--dataset-type", help="lerobot_v3 or sarm_progress"
|
|
25
|
+
),
|
|
26
|
+
flow: str = typer.Option(None, "--flow", help="input or output"),
|
|
27
|
+
project_id: str = typer.Option(None, "--project", help="Filter by project ID"),
|
|
28
|
+
job_id: str = typer.Option(None, "--job", help="Filter by job ID"),
|
|
29
|
+
json: bool = typer.Option(False, "--json", help="JSON output"),
|
|
30
|
+
) -> None:
|
|
31
|
+
"""List datasets (paginated)."""
|
|
32
|
+
client = get_client()
|
|
33
|
+
page = client.datasets.list(
|
|
34
|
+
limit=limit,
|
|
35
|
+
cursor=cursor,
|
|
36
|
+
sort=sort,
|
|
37
|
+
search=search,
|
|
38
|
+
subtype=subtype,
|
|
39
|
+
flow=flow,
|
|
40
|
+
project_id=UUID(project_id) if project_id else None,
|
|
41
|
+
job_id=UUID(job_id) if job_id else None,
|
|
42
|
+
)
|
|
43
|
+
if json:
|
|
44
|
+
print_json(page)
|
|
45
|
+
else:
|
|
46
|
+
rows = [
|
|
47
|
+
{
|
|
48
|
+
"id": str(d.artifact_id)[:8],
|
|
49
|
+
"subtype": d.subtype,
|
|
50
|
+
"path": d.path,
|
|
51
|
+
"jobs": len(d.jobs),
|
|
52
|
+
"created": d.created_at.strftime("%Y-%m-%d"),
|
|
53
|
+
}
|
|
54
|
+
for d in page.items
|
|
55
|
+
]
|
|
56
|
+
print_table(
|
|
57
|
+
rows,
|
|
58
|
+
[
|
|
59
|
+
("id", "Dataset ID"),
|
|
60
|
+
("subtype", "Subtype"),
|
|
61
|
+
("path", "Path"),
|
|
62
|
+
("jobs", "Jobs"),
|
|
63
|
+
("created", "Created"),
|
|
64
|
+
],
|
|
65
|
+
)
|
|
66
|
+
if page.page_info.has_next:
|
|
67
|
+
typer.echo(f"\nNext page: --cursor {page.page_info.end_cursor}")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@app.command("get-image-keys")
|
|
71
|
+
def get_image_keys(
|
|
72
|
+
dataset_id: str = typer.Argument(help="HuggingFace dataset ID"),
|
|
73
|
+
json: bool = typer.Option(False, "--json", help="JSON output"),
|
|
74
|
+
) -> None:
|
|
75
|
+
"""Get available image keys for camera mapping."""
|
|
76
|
+
client = get_client()
|
|
77
|
+
result = client.datasets.get_image_keys(dataset_id)
|
|
78
|
+
if json:
|
|
79
|
+
print_json(result)
|
|
80
|
+
else:
|
|
81
|
+
print_table(
|
|
82
|
+
[{"key": k} for k in result.image_keys],
|
|
83
|
+
[("key", "Image Key")],
|
|
84
|
+
title=f"Image keys for {result.dataset_id}",
|
|
85
|
+
)
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Job commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json as json_mod
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from qualia_cli.client import get_client
|
|
10
|
+
from qualia_cli.output import console, print_error, print_json, print_table
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(help="Manage fine-tuning jobs.")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@app.command("list")
|
|
16
|
+
def list_jobs(
|
|
17
|
+
limit: int = typer.Option(20, "--limit", "-n", help="Page size (1-100)"),
|
|
18
|
+
cursor: str = typer.Option(None, "--cursor", help="Pagination cursor"),
|
|
19
|
+
sort: str = typer.Option("newest", "--sort", "-s", help="newest|oldest|updated"),
|
|
20
|
+
search: str = typer.Option(None, "--search", "-q", help="Search job description"),
|
|
21
|
+
project_id: str = typer.Option(None, "--project", help="Filter by project ID"),
|
|
22
|
+
vla_type: str = typer.Option(None, "--vla-type", help="Filter by VLA type"),
|
|
23
|
+
job_type: str = typer.Option(None, "--job-type", help="vla|reward|vla_w_reward"),
|
|
24
|
+
use_json: bool = typer.Option(False, "--json", help="JSON output"),
|
|
25
|
+
) -> None:
|
|
26
|
+
"""List fine-tuning jobs (paginated)."""
|
|
27
|
+
client = get_client()
|
|
28
|
+
page = client.jobs.list(
|
|
29
|
+
limit=limit,
|
|
30
|
+
cursor=cursor,
|
|
31
|
+
sort=sort,
|
|
32
|
+
project_id=project_id,
|
|
33
|
+
vla_type=vla_type,
|
|
34
|
+
job_type=job_type,
|
|
35
|
+
search=search,
|
|
36
|
+
)
|
|
37
|
+
if use_json:
|
|
38
|
+
print_json(page)
|
|
39
|
+
else:
|
|
40
|
+
rows = [
|
|
41
|
+
{
|
|
42
|
+
"job_id": str(j.job_id)[:8],
|
|
43
|
+
"desc": j.job_desc or "",
|
|
44
|
+
"phase": j.current_phase or "",
|
|
45
|
+
"vla": j.vla_type or "",
|
|
46
|
+
"model": j.model or "",
|
|
47
|
+
"dataset": j.dataset or "",
|
|
48
|
+
"created": j.created_at.strftime("%Y-%m-%d") if j.created_at else "",
|
|
49
|
+
}
|
|
50
|
+
for j in page.items
|
|
51
|
+
]
|
|
52
|
+
print_table(
|
|
53
|
+
rows,
|
|
54
|
+
[
|
|
55
|
+
("job_id", "Job ID"),
|
|
56
|
+
("desc", "Description"),
|
|
57
|
+
("phase", "Phase"),
|
|
58
|
+
("vla", "VLA Type"),
|
|
59
|
+
("model", "Model"),
|
|
60
|
+
("dataset", "Dataset"),
|
|
61
|
+
("created", "Created"),
|
|
62
|
+
],
|
|
63
|
+
)
|
|
64
|
+
if page.page_info.has_next:
|
|
65
|
+
typer.echo(f"\nNext page: --cursor {page.page_info.end_cursor}")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@app.command("launch")
|
|
69
|
+
def launch(
|
|
70
|
+
project_id: str = typer.Option(..., help="Project ID"),
|
|
71
|
+
vla_type: str = typer.Option(
|
|
72
|
+
..., help="VLA type (act, smolvla, pi0, pi05, gr00t_n1_5)"
|
|
73
|
+
),
|
|
74
|
+
dataset_id: str = typer.Option(..., help="HuggingFace dataset ID"),
|
|
75
|
+
hours: float = typer.Option(..., help="Training hours"),
|
|
76
|
+
camera_mappings: str = typer.Option(
|
|
77
|
+
...,
|
|
78
|
+
help='Camera mappings as JSON, e.g. \'{"cam_1": "observation.images.top"}\'',
|
|
79
|
+
),
|
|
80
|
+
model_id: str | None = typer.Option(
|
|
81
|
+
None, help="HuggingFace model ID (required for smolvla, pi0, pi05)"
|
|
82
|
+
),
|
|
83
|
+
instance_type: str | None = typer.Option(None, help="GPU instance type"),
|
|
84
|
+
region: str | None = typer.Option(None, help="Cloud region"),
|
|
85
|
+
batch_size: int = typer.Option(32, help="Training batch size"),
|
|
86
|
+
name: str | None = typer.Option(None, help="Job name"),
|
|
87
|
+
hyper_spec: str | None = typer.Option(None, help="Custom hyperparams as JSON"),
|
|
88
|
+
use_json: bool = typer.Option(False, "--json", help="JSON output"),
|
|
89
|
+
) -> None:
|
|
90
|
+
"""Launch a new fine-tuning job."""
|
|
91
|
+
try:
|
|
92
|
+
cam_map = json_mod.loads(camera_mappings)
|
|
93
|
+
except json_mod.JSONDecodeError as e:
|
|
94
|
+
print_error("Invalid JSON for --camera-mappings")
|
|
95
|
+
raise SystemExit(1) from e
|
|
96
|
+
|
|
97
|
+
kwargs: dict = {
|
|
98
|
+
"project_id": project_id,
|
|
99
|
+
"vla_type": vla_type,
|
|
100
|
+
"dataset_id": dataset_id,
|
|
101
|
+
"hours": hours,
|
|
102
|
+
"camera_mappings": cam_map,
|
|
103
|
+
"batch_size": batch_size,
|
|
104
|
+
}
|
|
105
|
+
if model_id:
|
|
106
|
+
kwargs["model_id"] = model_id
|
|
107
|
+
if instance_type:
|
|
108
|
+
kwargs["instance_type"] = instance_type
|
|
109
|
+
if region:
|
|
110
|
+
kwargs["region"] = region
|
|
111
|
+
if name:
|
|
112
|
+
kwargs["name"] = name
|
|
113
|
+
if hyper_spec:
|
|
114
|
+
try:
|
|
115
|
+
kwargs["vla_hyper_spec"] = json_mod.loads(hyper_spec)
|
|
116
|
+
except json_mod.JSONDecodeError as e:
|
|
117
|
+
print_error("Invalid JSON for --hyper-spec")
|
|
118
|
+
raise SystemExit(1) from e
|
|
119
|
+
|
|
120
|
+
client = get_client()
|
|
121
|
+
job = client.finetune.create(**kwargs)
|
|
122
|
+
if use_json:
|
|
123
|
+
print_json(job)
|
|
124
|
+
else:
|
|
125
|
+
typer.echo(f"Job launched: {job.job_id} (status: {job.status})")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@app.command("get")
|
|
129
|
+
def get_status(
|
|
130
|
+
job_id: str = typer.Argument(help="Job ID"),
|
|
131
|
+
use_json: bool = typer.Option(False, "--json", help="JSON output"),
|
|
132
|
+
) -> None:
|
|
133
|
+
"""Get job status."""
|
|
134
|
+
client = get_client()
|
|
135
|
+
status = client.finetune.get(job_id)
|
|
136
|
+
if use_json:
|
|
137
|
+
print_json(status)
|
|
138
|
+
else:
|
|
139
|
+
console.print(f"[bold]Job:[/bold] {status.job_id}")
|
|
140
|
+
console.print(f"[bold]Status:[/bold] {status.status}")
|
|
141
|
+
console.print(f"[bold]Phase:[/bold] {status.current_phase}")
|
|
142
|
+
if status.instance_type:
|
|
143
|
+
console.print(f"[bold]Instance:[/bold] {status.instance_type}")
|
|
144
|
+
if status.region:
|
|
145
|
+
console.print(f"[bold]Region:[/bold] {status.region}")
|
|
146
|
+
if status.phases:
|
|
147
|
+
console.print()
|
|
148
|
+
rows = [
|
|
149
|
+
{
|
|
150
|
+
"name": p.name,
|
|
151
|
+
"status": p.status,
|
|
152
|
+
"started": str(p.started_at or "-"),
|
|
153
|
+
"completed": str(p.completed_at or "-"),
|
|
154
|
+
"error": p.error or "",
|
|
155
|
+
}
|
|
156
|
+
for p in status.phases
|
|
157
|
+
]
|
|
158
|
+
print_table(
|
|
159
|
+
rows,
|
|
160
|
+
[
|
|
161
|
+
("name", "Phase"),
|
|
162
|
+
("status", "Status"),
|
|
163
|
+
("started", "Started"),
|
|
164
|
+
("completed", "Completed"),
|
|
165
|
+
("error", "Error"),
|
|
166
|
+
],
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@app.command("cancel")
|
|
171
|
+
def cancel(
|
|
172
|
+
job_id: str = typer.Argument(help="Job ID to cancel"),
|
|
173
|
+
use_json: bool = typer.Option(False, "--json", help="JSON output"),
|
|
174
|
+
) -> None:
|
|
175
|
+
"""Cancel a job."""
|
|
176
|
+
client = get_client()
|
|
177
|
+
result = client.finetune.cancel(job_id)
|
|
178
|
+
if use_json:
|
|
179
|
+
print_json(result)
|
|
180
|
+
else:
|
|
181
|
+
if result.cancelled:
|
|
182
|
+
typer.echo(f"Cancelled job {result.job_id}")
|
|
183
|
+
else:
|
|
184
|
+
typer.echo(f"Could not cancel job {result.job_id}: {result.message}")
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@app.command("hyperparams")
|
|
188
|
+
def hyperparams(
|
|
189
|
+
vla_type: str = typer.Argument(help="VLA type"),
|
|
190
|
+
model_id: str | None = typer.Option(
|
|
191
|
+
None, help="Model ID for type-specific defaults"
|
|
192
|
+
),
|
|
193
|
+
) -> None:
|
|
194
|
+
"""Show default hyperparameters for a VLA type."""
|
|
195
|
+
client = get_client()
|
|
196
|
+
defaults = client.finetune.get_hyperparams_defaults(
|
|
197
|
+
vla_type=vla_type,
|
|
198
|
+
model_id=model_id,
|
|
199
|
+
)
|
|
200
|
+
print_json(defaults)
|