hyperstudy-perception 0.1.0__tar.gz → 0.1.1__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.
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/PKG-INFO +1 -1
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/pyproject.toml +1 -1
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/RELEASE.json +1 -1
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/backends/remote.py +2 -2
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/cli.py +44 -8
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/export.py +11 -1
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/hosts.py +74 -15
- hyperstudy_perception-0.1.1/src/perception/keycheck.py +64 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/LICENSE +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/README.md +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/__init__.py +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/cloud/runpod/guard.sh +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/cloud/runpod/image/Dockerfile +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/cloud/runpod/image/start.sh +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/cloud/runpod/image/tag.py +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/cloud/skypilot/gpu-faces.yaml +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/cloud/skypilot/gpu-tune.yaml +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/cloud/skypilot/guard.sh +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/cloud/skypilot/iam-perception-node.json +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/cloud/skypilot/launch.sh +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/cloud/skypilot/node-gpu.yaml +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/cloud/skypilot/node-setup.sh +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/cloud/skypilot/sky.yaml +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/cloud/skypilot/smoke.yaml +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/envs/api/pyproject.toml +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/envs/api/uv.lock +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/envs/audio/pyproject.toml +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/envs/audio/uv.lock +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/envs/core/pyproject.toml +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/envs/core/uv.lock +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/envs/faces/pyproject.toml +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/envs/faces/uv.lock +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/graphs/decode.yaml +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/graphs/faces.yaml +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/graphs/full.yaml +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/graphs/mvp-captions.yaml +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/graphs/mvp.yaml +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/nodes/audio/asr/manifest.yaml +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/nodes/audio/asr/node.py +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/nodes/audio/asr/prepare.py +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/nodes/audio/asr/test_wav.py +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/nodes/audio/asr/wav.py +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/nodes/conftest.py +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/nodes/core/decode/manifest.yaml +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/nodes/core/decode/media.py +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/nodes/core/decode/node.py +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/nodes/core/decode/test_media.py +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/nodes/testdata/README.md +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/nodes/testdata/rotated-90.mp4 +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/nodes/vision/face-track/frames.py +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/nodes/vision/face-track/loader.py +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/nodes/vision/face-track/manifest.yaml +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/nodes/vision/face-track/node.py +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/nodes/vision/face-track/prepare.py +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/nodes/vision/face-track/test_frames.py +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/nodes/vision/face-track/test_loader.py +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/nodes/vision/face-track/test_thumbnails.py +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/nodes/vision/face-track/thumbnails.py +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/nodes/vision/face-track/tracking.py +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/nodes/vlm/shot-captions/manifest.yaml +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/nodes/vlm/shot-captions/node.py +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/schema/README.md +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/schema/bundle-0.1.schema.json +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/sdk/pyproject.toml +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/sdk/src/perception_node/__init__.py +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/artifacts.py +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/backends/__init__.py +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/backends/base.py +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/backends/cloud.py +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/backends/native.py +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/backends/probe.py +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/estimates.py +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/ir.py +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/keys.py +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/manifests.py +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/plan.py +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/release.py +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/remote_driver.py +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/runpod.py +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/scheduler.py +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/setup.py +0 -0
- {hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/store.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: hyperstudy-perception
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: Video perception pipeline: shots, speech, faces and captions from a video, as a versioned track bundle, run locally, on cloud GPUs or lab servers
|
|
5
5
|
Project-URL: Homepage, https://github.com/hyperstudyio/hyperstudy-perception
|
|
6
6
|
Project-URL: Changelog, https://github.com/hyperstudyio/hyperstudy-perception/releases
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "hyperstudy-perception"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.1"
|
|
4
4
|
description = "Video perception pipeline: shots, speech, faces and captions from a video, as a versioned track bundle, run locally, on cloud GPUs or lab servers"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
{hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/backends/remote.py
RENAMED
|
@@ -22,7 +22,7 @@ import subprocess
|
|
|
22
22
|
import sys
|
|
23
23
|
from pathlib import Path
|
|
24
24
|
|
|
25
|
-
from ..hosts import Host
|
|
25
|
+
from ..hosts import Host
|
|
26
26
|
from ..manifests import Manifest, Runner
|
|
27
27
|
from ..release import files_in, release
|
|
28
28
|
from .base import Capabilities
|
|
@@ -127,7 +127,7 @@ class SshBackend(NativeBackend):
|
|
|
127
127
|
files = code_files(manifest, runner)
|
|
128
128
|
caps = self.host.caps
|
|
129
129
|
spec = {
|
|
130
|
-
"dest": self.host.dest, "root": self.host.root, "host": self.host.name, "ssh":
|
|
130
|
+
"dest": self.host.dest, "root": self.host.root, "host": self.host.name, "ssh": self.host.ssh(),
|
|
131
131
|
"code": code_hash(files), "files": files,
|
|
132
132
|
"node_dir": manifest.directory.resolve().relative_to(REPO).as_posix(), "entry": runner.entry, "env": runner.env,
|
|
133
133
|
"runner_kind": self.runner_kind, "python": self.python, "uv": caps.tools.get("uv"), "prepare": manifest.prepare,
|
|
@@ -41,7 +41,7 @@ from .backends.native import NativeBackend, SelfBackend
|
|
|
41
41
|
from .backends.probe import probe
|
|
42
42
|
from .backends.cloud import Cloud, CloudBackend, RunPodBackend
|
|
43
43
|
from .backends.remote import SshBackend
|
|
44
|
-
from .hosts import HostError, Hosts,
|
|
44
|
+
from .hosts import HostError, Hosts, probe_dest
|
|
45
45
|
from .keys import load_secrets
|
|
46
46
|
from .ir import GraphError, compile_graph
|
|
47
47
|
from .manifests import Registry
|
|
@@ -258,8 +258,13 @@ def cmd_nodes(args):
|
|
|
258
258
|
def cmd_models(args):
|
|
259
259
|
reg = registry(args)
|
|
260
260
|
if args.json:
|
|
261
|
+
# each node too: what it is called, the API it calls (service) and the keys it needs, and its fixed choices
|
|
262
|
+
# (a model, an effort) at their defaults, with prices: what the app's settings show
|
|
263
|
+
from .plan import settings
|
|
261
264
|
emit("models", models=[{"node": m.ref, **x.model_dump(include={"id", "source", "size_gb", "gated", "licence_mode"})}
|
|
262
|
-
for m in reg for x in m.models]
|
|
265
|
+
for m in reg for x in m.models],
|
|
266
|
+
nodes=[{"node": m.ref, "title": m.title, "service": m.service, "secrets": list(m.secrets),
|
|
267
|
+
"settings": settings(m, {k: s.get("default") for k, s in m.config.items()})} for m in reg])
|
|
263
268
|
return 0
|
|
264
269
|
for m in reg:
|
|
265
270
|
for x in m.models:
|
|
@@ -481,7 +486,18 @@ def cmd_hosts(args):
|
|
|
481
486
|
if args.action == "add":
|
|
482
487
|
if not args.dest:
|
|
483
488
|
raise HostError("hosts add NAME USER@HOST")
|
|
484
|
-
hosts.add(args.name, args.dest, args.root)
|
|
489
|
+
hosts.add(args.name, args.dest, args.root, args.user, args.port, args.identity)
|
|
490
|
+
elif args.action == "probe": # a connection test: nothing is added, nothing changes on the host
|
|
491
|
+
dest = args.dest or args.name
|
|
492
|
+
if not dest:
|
|
493
|
+
raise HostError("hosts probe USER@HOST")
|
|
494
|
+
caps, root = probe_dest(dest, args.root, args.user, args.port, args.identity)
|
|
495
|
+
found = {"dest": dest, "root": root, "caps": {**caps.__dict__, "accelerators": sorted(caps.accelerators)}}
|
|
496
|
+
if args.json:
|
|
497
|
+
emit("host_probe", **found)
|
|
498
|
+
else:
|
|
499
|
+
print(f"{dest}: {caps.platform}, {caps.cpu_cores} cores, {caps.memory_gb} GB, {'/'.join(sorted(caps.accelerators))}")
|
|
500
|
+
return 0
|
|
485
501
|
elif args.action in ("check", "remove", "clean") and not args.name:
|
|
486
502
|
raise HostError(f"hosts {args.action} NAME")
|
|
487
503
|
elif args.action == "check":
|
|
@@ -510,7 +526,7 @@ def cmd_hosts(args):
|
|
|
510
526
|
f'if ps -ww -p "$p" -o command= 2>/dev/null | grep -qF -- "${{f%/pid}}/request.json"; then '
|
|
511
527
|
f'echo "a node is running there" >&2; exit 4; fi; done\n'
|
|
512
528
|
f"chmod -R u+w {root}/objects 2>/dev/null; rm -rf {root}/objects {root}/code {root}/work")
|
|
513
|
-
out = subprocess.run([*
|
|
529
|
+
out = subprocess.run([*host.ssh(), host.dest, f"sh -c {q(script)}"], capture_output=True, text=True, timeout=600)
|
|
514
530
|
if out.returncode:
|
|
515
531
|
raise HostError(f"could not clean {host.name}: {out.stderr.strip()[-300:]}")
|
|
516
532
|
except HostError as e:
|
|
@@ -536,6 +552,13 @@ def cmd_cloud(args):
|
|
|
536
552
|
(SkyPilot, your AWS credentials) or RunPod (an API key). `on` checks that the provider can be used first."""
|
|
537
553
|
cloud = Cloud(home())
|
|
538
554
|
problem = None
|
|
555
|
+
if args.action == "check": # whether it could be used, changing nothing (a test)
|
|
556
|
+
problem = runpod_problem() if args.provider == "runpod" else aws_problem()
|
|
557
|
+
if args.json:
|
|
558
|
+
emit("cloud_check", provider=args.provider, ok=problem is None, detail=problem or f"{args.provider} can be used")
|
|
559
|
+
else:
|
|
560
|
+
print(problem or f"{args.provider} can be used")
|
|
561
|
+
return 0
|
|
539
562
|
if args.action == "on":
|
|
540
563
|
problem = runpod_problem() if args.provider == "runpod" else aws_problem()
|
|
541
564
|
if problem is None:
|
|
@@ -609,6 +632,16 @@ def cmd_keys(args):
|
|
|
609
632
|
if not args.name:
|
|
610
633
|
raise ValueError("keys remove NAME: which key?")
|
|
611
634
|
result = {"name": args.name, "removed": keys.remove(args.name)}
|
|
635
|
+
elif args.action == "check": # whether its provider accepts it: a refused key is an answer, not a failure
|
|
636
|
+
if not args.name:
|
|
637
|
+
raise ValueError("keys check NAME: which key?")
|
|
638
|
+
from .keycheck import check
|
|
639
|
+
ok, detail = check(args.name, keys.load().values.get(args.name))
|
|
640
|
+
if args.json:
|
|
641
|
+
emit("key_check", name=args.name, ok=ok, detail=detail)
|
|
642
|
+
else:
|
|
643
|
+
print(detail)
|
|
644
|
+
return 0
|
|
612
645
|
else:
|
|
613
646
|
found = keys.load()
|
|
614
647
|
result = {"keys": [{"name": k, "source": found.sources[k]} for k in sorted(found.values)], "problems": found.problems}
|
|
@@ -743,17 +776,20 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
743
776
|
rr.add_argument("--allow-uploads", action="store_true", help="let the run's nodes send data off this machine from now on")
|
|
744
777
|
rr.set_defaults(fn=cmd_rerun)
|
|
745
778
|
h = sub.add_parser("hosts", help="lab servers nodes can run on, over SSH")
|
|
746
|
-
h.add_argument("action", choices=["add", "list", "check", "remove", "clean"])
|
|
747
|
-
h.add_argument("name", nargs="?")
|
|
779
|
+
h.add_argument("action", choices=["add", "list", "check", "remove", "clean", "probe"])
|
|
780
|
+
h.add_argument("name", nargs="?", help="probe: the ssh destination")
|
|
748
781
|
h.add_argument("dest", nargs="?", help="add: an ssh destination (user@host, or a ~/.ssh/config alias)")
|
|
749
782
|
h.add_argument("--root", default=".cache/hyperstudy-perception", help="add: where on the host its files go (under its home if relative)")
|
|
783
|
+
h.add_argument("--user", help="add, probe: the user to log in as, over what ~/.ssh/config says")
|
|
784
|
+
h.add_argument("--port", type=int, help="add, probe: the ssh port, over what ~/.ssh/config says")
|
|
785
|
+
h.add_argument("--identity", help="add, probe: a private key file on this machine, over what ~/.ssh/config says")
|
|
750
786
|
h.set_defaults(fn=cmd_hosts)
|
|
751
787
|
c = sub.add_parser("cloud", help="offer a cloud GPU machine to nodes that can use one")
|
|
752
|
-
c.add_argument("action", choices=["on", "off", "status"])
|
|
788
|
+
c.add_argument("action", choices=["on", "off", "status", "check"])
|
|
753
789
|
c.add_argument("provider", nargs="?", choices=Cloud.providers, default="aws")
|
|
754
790
|
c.set_defaults(fn=cmd_cloud)
|
|
755
791
|
k = sub.add_parser("keys", help="your API keys and tokens: store one in the keychain, list where each comes from")
|
|
756
|
-
k.add_argument("action", choices=["set", "list", "remove"])
|
|
792
|
+
k.add_argument("action", choices=["set", "list", "remove", "check"])
|
|
757
793
|
k.add_argument("name", nargs="?")
|
|
758
794
|
k.set_defaults(fn=cmd_keys)
|
|
759
795
|
st = sub.add_parser("setup", help="install what nodes need here before a run: environments and weights")
|
|
@@ -20,6 +20,16 @@ from .scheduler import Coordinator, LICENCE_ORDER
|
|
|
20
20
|
SCHEMA_VERSION = "0.1.0"
|
|
21
21
|
|
|
22
22
|
|
|
23
|
+
def _version() -> str:
|
|
24
|
+
"""The installed package's version: what made the bundle ("unknown" in a checkout not installed; its git_commit
|
|
25
|
+
says which)."""
|
|
26
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
27
|
+
try:
|
|
28
|
+
return version("hyperstudy-perception")
|
|
29
|
+
except PackageNotFoundError:
|
|
30
|
+
return "unknown"
|
|
31
|
+
|
|
32
|
+
|
|
23
33
|
def _git_commit() -> str | None:
|
|
24
34
|
if r := release():
|
|
25
35
|
return r["commit"][:7]
|
|
@@ -92,7 +102,7 @@ def bundle(coord: Coordinator, run_id: str, out: Path) -> Path:
|
|
|
92
102
|
"format": "hyperstudy-perception-bundle", "schema_version": SCHEMA_VERSION,
|
|
93
103
|
"created_at": time.strftime("%Y-%m-%dT%H:%M:%S+00:00", time.gmtime()),
|
|
94
104
|
"video": {k: media[k] for k in ("sha256", "duration", "fps", "width", "height")} | ({"filename": media["filename"]} if media.get("filename") else {}),
|
|
95
|
-
"pipeline": {"name": "hyperstudy-perception", "version":
|
|
105
|
+
"pipeline": {"name": "hyperstudy-perception", "version": _version(), "git_commit": _git_commit(), "source": f"perception run {run_id}"},
|
|
96
106
|
"layers": entries,
|
|
97
107
|
"assets": assets,
|
|
98
108
|
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
"""Other machines nodes can run on: lab servers reached over SSH (`perception hosts add NAME DEST`).
|
|
2
2
|
|
|
3
|
+
DEST is what you would give ssh (user@host, or a Host from ~/.ssh/config). --user, --port and --identity override what
|
|
4
|
+
~/.ssh/config says for it; every command sent to the host uses them (Host.ssh). `perception hosts probe DEST` reaches a
|
|
5
|
+
destination and says what it has without adding it or changing anything there (a connection test).
|
|
6
|
+
|
|
3
7
|
A host is probed when it is added (and by `hosts check`): its platform, cores, memory, GPU and tools are recorded in
|
|
4
8
|
$PERCEPTION_HOME/hosts.json, so planning never waits on the network. SSH runs in batch mode with the user's own keys
|
|
5
9
|
and ~/.ssh/config: nothing here asks for or stores a password. Connections are shared (ControlMaster), so the many short
|
|
@@ -36,6 +40,7 @@ DEST = re.compile(r"^[A-Za-z0-9_][A-Za-z0-9_.@%+:\[\]-]*$")
|
|
|
36
40
|
ROOT = re.compile(r"^[A-Za-z0-9_./~-]+$")
|
|
37
41
|
DEFAULT_ROOT = ".cache/hyperstudy-perception"
|
|
38
42
|
RESERVED = {"native", "self", "cloud", "runpod", "local"}
|
|
43
|
+
USER = re.compile(r"^[A-Za-z0-9._][A-Za-z0-9._-]{0,63}$")
|
|
39
44
|
|
|
40
45
|
# What the host has, as key=value lines. POSIX sh: Linux servers and Macs, no Python needed. Tools are looked for on the
|
|
41
46
|
# PATH a login shell would add too (~/.local/bin is where uv installs itself), since ssh commands get a minimal PATH.
|
|
@@ -65,6 +70,10 @@ class Host:
|
|
|
65
70
|
root: str # absolute, on the host
|
|
66
71
|
caps: Capabilities
|
|
67
72
|
probed_at: float
|
|
73
|
+
# overrides of what ~/.ssh/config says for dest (None: ssh's own)
|
|
74
|
+
user: str | None = None
|
|
75
|
+
port: int | None = None
|
|
76
|
+
identity: str | None = None # a private key on this machine, absolute
|
|
68
77
|
|
|
69
78
|
def to_json(self) -> dict:
|
|
70
79
|
d = asdict(self)
|
|
@@ -74,20 +83,59 @@ class Host:
|
|
|
74
83
|
@classmethod
|
|
75
84
|
def from_json(cls, d: dict) -> "Host":
|
|
76
85
|
caps = Capabilities(**{**d["caps"], "accelerators": set(d["caps"]["accelerators"])})
|
|
77
|
-
return cls(d["name"], d["dest"], d["root"], caps, d["probed_at"])
|
|
86
|
+
return cls(d["name"], d["dest"], d["root"], caps, d["probed_at"], d.get("user"), d.get("port"), d.get("identity"))
|
|
87
|
+
|
|
88
|
+
def ssh(self, fresh: bool = False) -> list[str]:
|
|
89
|
+
"""ssh to this host: every command sent to it (probe, nodes, clean) goes through this, with its overrides.
|
|
90
|
+
`fresh`: its own connection, not a shared one (a check: see ssh_command)."""
|
|
91
|
+
return [*ssh_command(shared=not fresh, identity=self.identity), *ssh_options(self.user, self.port, self.identity)]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def ssh_options(user: str | None, port: int | None, identity: str | None) -> list[str]:
|
|
95
|
+
return [*(["-l", user] if user else []), *(["-p", str(port)] if port else []),
|
|
96
|
+
*(["-i", identity, "-o", "IdentitiesOnly=yes"] if identity else [])]
|
|
78
97
|
|
|
79
98
|
|
|
80
|
-
def
|
|
99
|
+
def check_overrides(dest: str, user: str | None, port: int | None, identity: str | None) -> str | None:
|
|
100
|
+
"""Refuse overrides ssh would misread; the identity file as an absolute path on this machine."""
|
|
101
|
+
if user is not None:
|
|
102
|
+
if not USER.match(user):
|
|
103
|
+
raise HostError(f"not a user name: {user!r}")
|
|
104
|
+
if "@" in dest:
|
|
105
|
+
raise HostError(f"{dest} already names its user: give the user once (in the destination, or --user)")
|
|
106
|
+
if port is not None and not 1 <= port <= 65535:
|
|
107
|
+
raise HostError(f"not a port: {port}")
|
|
108
|
+
if identity is None:
|
|
109
|
+
return None
|
|
110
|
+
path = Path(identity).expanduser()
|
|
111
|
+
if not path.is_file():
|
|
112
|
+
raise HostError(f"no key file at {identity}")
|
|
113
|
+
path = path.resolve()
|
|
114
|
+
if any(ord(c) < 32 for c in str(path)):
|
|
115
|
+
raise HostError(f"not a plain path: {identity!r}")
|
|
116
|
+
if "%" in str(path) or "${" in str(path): # ssh expands %u, ${HOME}… in -i: it would read another file than this
|
|
117
|
+
raise HostError(f"ssh would expand % or ${{ in a key file's path: rename {identity}")
|
|
118
|
+
return str(path)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def ssh_command(shared: bool = True, identity: str | None = None) -> list[str]:
|
|
81
122
|
"""ssh with the options every remote command uses. PERCEPTION_SSH replaces the ssh program (the tests use a
|
|
82
|
-
stand-in that runs commands on this machine).
|
|
123
|
+
stand-in that runs commands on this machine). A connection is shared by host, port and user (%C), and by key file
|
|
124
|
+
too (`identity`): ssh's own sharing ignores the key, so a connection opened with one key would carry commands meant
|
|
125
|
+
for another. Not `shared`: a connection of its own, for a check (add, probe, check), whose yes must be this
|
|
126
|
+
connection's, never one another key opened."""
|
|
83
127
|
control = Path(f"/tmp/perception-ssh-{os.getuid()}") # short: a socket path must fit in 104 bytes on macOS
|
|
84
128
|
control.mkdir(mode=0o700, exist_ok=True)
|
|
85
129
|
st = control.stat()
|
|
86
130
|
if st.st_uid != os.getuid() or st.st_mode & 0o077:
|
|
87
131
|
raise HostError(f"{control} must be a directory only you can use")
|
|
88
|
-
|
|
89
|
-
"-o", "ServerAliveInterval=15", "-o", "ServerAliveCountMax=4"
|
|
90
|
-
|
|
132
|
+
base = [os.environ.get("PERCEPTION_SSH", "ssh"), "-o", "BatchMode=yes", "-o", "ConnectTimeout=15",
|
|
133
|
+
"-o", "ServerAliveInterval=15", "-o", "ServerAliveCountMax=4"]
|
|
134
|
+
if not shared:
|
|
135
|
+
return [*base, "-o", "ControlMaster=no", "-o", "ControlPath=none"]
|
|
136
|
+
import hashlib
|
|
137
|
+
keyed = f"-{hashlib.sha256(identity.encode()).hexdigest()[:12]}" if identity else "" # short: the socket path's limit
|
|
138
|
+
return [*base, "-o", "ControlMaster=auto", "-o", f"ControlPath={control}/%C{keyed}", "-o", "ControlPersist=120"]
|
|
91
139
|
|
|
92
140
|
|
|
93
141
|
def check_names(name: str, dest: str, root: str):
|
|
@@ -126,11 +174,12 @@ def private_root_script(root: str) -> str:
|
|
|
126
174
|
return f"root={path}\n" + PRIVATE_ROOT
|
|
127
175
|
|
|
128
176
|
|
|
129
|
-
def probe(dest: str, root: str = DEFAULT_ROOT, ssh: list[str] | None = None) -> tuple[Capabilities, str]:
|
|
177
|
+
def probe(dest: str, root: str = DEFAULT_ROOT, ssh: list[str] | None = None, make_root: bool = True) -> tuple[Capabilities, str]:
|
|
130
178
|
"""What the host has, and its root as an absolute path (made private there, or refused: PRIVATE_ROOT). `ssh`: the
|
|
131
|
-
command to reach it, when it needs options of its own (a RunPod pod's key and port).
|
|
132
|
-
|
|
133
|
-
|
|
179
|
+
command to reach it, when it needs options of its own (overrides, a RunPod pod's key and port). Without `make_root`
|
|
180
|
+
nothing on the host is changed: the root is only where it would be (a connection test)."""
|
|
181
|
+
script = (private_root_script(root) if make_root else "") + PROBE
|
|
182
|
+
out = subprocess.run([*(ssh or ssh_command()), dest, "sh -s"], input=script, capture_output=True, text=True, timeout=120)
|
|
134
183
|
if out.returncode:
|
|
135
184
|
raise HostError(f"could not reach {dest} (ssh exit {out.returncode}): {out.stderr.strip()[-500:]}")
|
|
136
185
|
facts = dict(line.split("=", 1) for line in out.stdout.splitlines() if "=" in line)
|
|
@@ -175,10 +224,12 @@ class Hosts:
|
|
|
175
224
|
tmp.write_text(json.dumps({n: h.to_json() for n, h in sorted(hosts.items())}, indent=1))
|
|
176
225
|
os.replace(tmp, self.path)
|
|
177
226
|
|
|
178
|
-
def add(self, name: str, dest: str, root: str = DEFAULT_ROOT
|
|
227
|
+
def add(self, name: str, dest: str, root: str = DEFAULT_ROOT, user: str | None = None, port: int | None = None,
|
|
228
|
+
identity: str | None = None) -> Host:
|
|
179
229
|
check_names(name, dest, root)
|
|
180
|
-
|
|
181
|
-
|
|
230
|
+
identity = check_overrides(dest, user, port, identity)
|
|
231
|
+
caps, absolute = probe(dest, root, [*ssh_command(shared=False), *ssh_options(user, port, identity)])
|
|
232
|
+
host = Host(name, dest, absolute, caps, time.time(), user, port, identity)
|
|
182
233
|
hosts = self.load()
|
|
183
234
|
hosts[name] = host
|
|
184
235
|
self.save(hosts)
|
|
@@ -189,8 +240,8 @@ class Hosts:
|
|
|
189
240
|
if name not in hosts:
|
|
190
241
|
raise HostError(f"no host {name!r} (perception hosts list)")
|
|
191
242
|
old = hosts[name]
|
|
192
|
-
caps, _ = probe(old.dest, old.root)
|
|
193
|
-
hosts[name] = Host(name, old.dest, old.root, caps, time.time())
|
|
243
|
+
caps, _ = probe(old.dest, old.root, old.ssh(fresh=True))
|
|
244
|
+
hosts[name] = Host(name, old.dest, old.root, caps, time.time(), old.user, old.port, old.identity)
|
|
194
245
|
self.save(hosts)
|
|
195
246
|
return hosts[name]
|
|
196
247
|
|
|
@@ -199,3 +250,11 @@ class Hosts:
|
|
|
199
250
|
if hosts.pop(name, None) is None:
|
|
200
251
|
raise HostError(f"no host {name!r}")
|
|
201
252
|
self.save(hosts)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def probe_dest(dest: str, root: str = DEFAULT_ROOT, user: str | None = None, port: int | None = None,
|
|
256
|
+
identity: str | None = None) -> tuple[Capabilities, str]:
|
|
257
|
+
"""Reach a destination as `hosts add` would and say what it has, changing nothing there and adding nothing here."""
|
|
258
|
+
check_names("probe", dest, root)
|
|
259
|
+
identity = check_overrides(dest, user, port, identity)
|
|
260
|
+
return probe(dest, root, [*ssh_command(shared=False), *ssh_options(user, port, identity)], make_root=False)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Whether a provider accepts a key (`perception keys check NAME`): one request that reads something small and costs
|
|
2
|
+
nothing (Anthropic's model list, your Hugging Face account, your RunPod pods). The key is sent only to its own
|
|
3
|
+
provider, never through a redirect (runpod.OPENER)."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import http.client
|
|
8
|
+
import json
|
|
9
|
+
import urllib.error
|
|
10
|
+
import urllib.request
|
|
11
|
+
|
|
12
|
+
from .runpod import OPENER, RunPod, RunPodError
|
|
13
|
+
|
|
14
|
+
ANTHROPIC = "https://api.anthropic.com/v1/models?limit=1"
|
|
15
|
+
HUGGING_FACE = "https://huggingface.co/api/whoami-v2"
|
|
16
|
+
CHECKED = ("ANTHROPIC_API_KEY", "HF_TOKEN", "RUNPOD_API_KEY")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get(url: str, headers: dict[str, str]) -> tuple[int, bytes]:
|
|
20
|
+
"""The status and body of a GET (an HTTP error's too); a provider that cannot be reached raises OSError."""
|
|
21
|
+
try:
|
|
22
|
+
with OPENER.open(urllib.request.Request(url, headers=headers), timeout=20) as r:
|
|
23
|
+
return r.status, r.read()
|
|
24
|
+
except urllib.error.HTTPError as e:
|
|
25
|
+
return e.code, e.read()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def account(body: bytes) -> str | None:
|
|
29
|
+
"""The account name in Hugging Face's answer, if it says one (the key was accepted either way)."""
|
|
30
|
+
try:
|
|
31
|
+
d = json.loads(body or b"{}")
|
|
32
|
+
except ValueError:
|
|
33
|
+
return None
|
|
34
|
+
return d.get("name") if isinstance(d, dict) else None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def check(name: str, value: str | None) -> tuple[bool, str]:
|
|
38
|
+
"""(accepted, what to tell the user)."""
|
|
39
|
+
if name not in CHECKED:
|
|
40
|
+
raise ValueError(f"no check for {name}: perception can check {', '.join(CHECKED)}")
|
|
41
|
+
if not value:
|
|
42
|
+
return False, f"{name} is not set"
|
|
43
|
+
try:
|
|
44
|
+
if name == "RUNPOD_API_KEY":
|
|
45
|
+
try:
|
|
46
|
+
RunPod(value).pods()
|
|
47
|
+
except RunPodError as e:
|
|
48
|
+
return False, ("RunPod refused the key" if e.status in (401, 403) else f"RunPod could not be checked: {e}")
|
|
49
|
+
return True, "RunPod accepted the key"
|
|
50
|
+
if name == "ANTHROPIC_API_KEY":
|
|
51
|
+
status, _ = get(ANTHROPIC, {"x-api-key": value, "anthropic-version": "2023-06-01"})
|
|
52
|
+
provider = "Anthropic"
|
|
53
|
+
else:
|
|
54
|
+
status, body = get(HUGGING_FACE, {"Authorization": f"Bearer {value}"})
|
|
55
|
+
provider = "Hugging Face"
|
|
56
|
+
if status == 200:
|
|
57
|
+
return True, f"Hugging Face accepted the key{f' (as {who})' if (who := account(body)) else ''}"
|
|
58
|
+
except (OSError, http.client.HTTPException) as e: # no whole answer (a reply cut short is not an OSError)
|
|
59
|
+
return False, f"could not reach the provider to check: {e}"
|
|
60
|
+
if status == 200:
|
|
61
|
+
return True, f"{provider} accepted the key"
|
|
62
|
+
if status in (401, 403):
|
|
63
|
+
return False, f"{provider} refused the key"
|
|
64
|
+
return False, f"{provider} could not be checked (HTTP {status})"
|
|
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
|
{hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/_pipeline/graphs/mvp.yaml
RENAMED
|
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
|
{hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/backends/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/backends/cloud.py
RENAMED
|
File without changes
|
{hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/backends/native.py
RENAMED
|
File without changes
|
{hyperstudy_perception-0.1.0 → hyperstudy_perception-0.1.1}/src/perception/backends/probe.py
RENAMED
|
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
|