auvux 0.1.0a1__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.
auvux-0.1.0a1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Auvux
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
auvux-0.1.0a1/PKG-INFO ADDED
@@ -0,0 +1,148 @@
1
+ Metadata-Version: 2.4
2
+ Name: auvux
3
+ Version: 0.1.0a1
4
+ Summary: Annotate faster. See deeper. Sound smarter.
5
+ Author-email: Auvux <peter@dreamteam.nl>
6
+ License: MIT
7
+ Project-URL: Homepage, https://inference.auvux.com
8
+ Project-URL: Repository, https://github.com/auvux/auvux-py
9
+ Project-URL: Issues, https://github.com/auvux/auvux-py/issues
10
+ Keywords: webrtc,jupyter,visualization,inference,audio
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Framework :: Jupyter
21
+ Classifier: Topic :: Multimedia :: Sound/Audio :: Analysis
22
+ Classifier: Topic :: Scientific/Engineering :: Visualization
23
+ Requires-Python: >=3.10
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: aiortc>=1.9.0
27
+ Requires-Dist: websockets>=12.0
28
+ Requires-Dist: numpy>=1.24
29
+ Requires-Dist: soundfile>=0.12
30
+ Dynamic: license-file
31
+
32
+ <p align="center">
33
+ <img src="https://raw.githubusercontent.com/auvux/auvux-py/main/assets/logo.png" alt="auvux" width="120" />
34
+ </p>
35
+
36
+ <h1 align="center">auvux</h1>
37
+
38
+ <p align="center"><em>Annotate faster. See deeper. Sound smarter.</em></p>
39
+
40
+ ---
41
+
42
+ Push inference runs from a Python notebook to a browser viewer over WebRTC.
43
+
44
+ Bytes flow peer-to-peer over a DataChannel; identical bytes across runs
45
+ and viewers are deduplicated via per-viewer SHA-256 content addressing.
46
+ Auth is GitHub OAuth (`user:email` scope) verified by an HMAC-signed
47
+ JWT — your tokens never leave your machine.
48
+
49
+ ## Install
50
+
51
+ ```bash
52
+ pip install --pre auvux
53
+ ```
54
+
55
+ (`--pre` while we're on the `0.1.0a1` alpha release.)
56
+
57
+ ## Use
58
+
59
+ ```bash
60
+ auvux login # GitHub Device Flow, caches a JWT in ~/.auvux/
61
+ auvux whoami # confirm
62
+ ```
63
+
64
+ ```python
65
+ import auvux
66
+ from auvux import lanes, markers
67
+
68
+ # Same as `auvux login`, but inline. Idempotent — no-op if already
69
+ # signed in.
70
+ auvux.login()
71
+
72
+ canvas = auvux.Canvas("Beat Detection")
73
+ canvas.add(
74
+ lanes.Waveform.from_path("demo.wav", size=160, label="mix"),
75
+ markers=[markers.vlines(beats, color="#FFFF00")],
76
+ )
77
+ canvas.add(lanes.Heatmap(spect, yaxis="mel", fmin=30, fmax=11000, size=256))
78
+ canvas.add(lanes.line(t, beat_logits, color="#4CAF50", label="beats", fill=True))
79
+ canvas.minimap(from_lane=0)
80
+ canvas.clicks(beats, freq=800)
81
+ canvas.show() # opens at https://inference.auvux.com
82
+ ```
83
+
84
+ Open `https://inference.auvux.com` in your browser **before** running
85
+ `canvas.show()` — the publisher waits up to 60 seconds for at least
86
+ one viewer to be present.
87
+
88
+ `Waveform.from_path()` accepts both local paths and `http(s)://` URLs;
89
+ remote files are fetched once into memory and reused without a second
90
+ round-trip on emit.
91
+
92
+ ## What gets sent
93
+
94
+ Each call to `canvas.show()` packages:
95
+
96
+ - A `manifest.json` describing the lanes and their layout
97
+ - Sidecar files: WAV / NPY / NPZ / CSV / JSON, per lane type
98
+ - Click-track time lists (if any)
99
+
100
+ The viewer pre-announces which file hashes it already has, so unchanged
101
+ audio across iterations is sent **once** — subsequent runs reuse the
102
+ cached blob.
103
+
104
+ ## Auth flow
105
+
106
+ `auvux.login()` runs the GitHub Device Flow with scope `user:email`:
107
+
108
+ 1. Asks GitHub for a short user code (`XXXX-XXXX`)
109
+ 2. Opens the browser to the verification URL
110
+ 3. Polls until you authorise
111
+ 4. Swaps the GitHub access token for an auvux JWT at
112
+ `https://inference.auvux.com/api/auth/cli-exchange`
113
+ 5. Caches the JWT in `~/.auvux/credentials.json` (chmod 600)
114
+
115
+ Subsequent `auvux.login()` calls are no-ops if the cached token is
116
+ still valid; pass `force=True` to re-authenticate.
117
+
118
+ ## Environment
119
+
120
+ | Var | Default | Purpose |
121
+ | --- | --- | --- |
122
+ | `AUVUX_SIGNALLING_URL` | from `~/.auvux/credentials.json`, otherwise `wss://signalling.auvux.com/signalling/me` | Override the signalling endpoint (escape hatch for local dev) |
123
+ | `AUVUX_API_BASE` | `https://inference.auvux.com` | Auvux API host used for `cli-exchange` |
124
+ | `AUVUX_GH_CLIENT_ID` | built-in | GitHub OAuth App client id |
125
+ | `AUVUX_PEER_WAIT` | `60` | Seconds to wait for a viewer before erroring |
126
+ | `AUVUX_ACK_WAIT` | `30` | Seconds to wait per-viewer for delivery ack |
127
+
128
+ ## Development
129
+
130
+ ```bash
131
+ git clone https://github.com/auvux/auvux-py
132
+ cd auvux-py
133
+ pip install -e .
134
+ ```
135
+
136
+ Release a new version: bump `version` in `pyproject.toml`, then
137
+
138
+ ```bash
139
+ git tag v0.1.0a2
140
+ git push --tags
141
+ ```
142
+
143
+ The `release.yml` workflow builds the wheel + sdist and publishes to
144
+ PyPI via OIDC (Trusted Publisher) — no API token in the repo.
145
+
146
+ ## License
147
+
148
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,117 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/auvux/auvux-py/main/assets/logo.png" alt="auvux" width="120" />
3
+ </p>
4
+
5
+ <h1 align="center">auvux</h1>
6
+
7
+ <p align="center"><em>Annotate faster. See deeper. Sound smarter.</em></p>
8
+
9
+ ---
10
+
11
+ Push inference runs from a Python notebook to a browser viewer over WebRTC.
12
+
13
+ Bytes flow peer-to-peer over a DataChannel; identical bytes across runs
14
+ and viewers are deduplicated via per-viewer SHA-256 content addressing.
15
+ Auth is GitHub OAuth (`user:email` scope) verified by an HMAC-signed
16
+ JWT — your tokens never leave your machine.
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ pip install --pre auvux
22
+ ```
23
+
24
+ (`--pre` while we're on the `0.1.0a1` alpha release.)
25
+
26
+ ## Use
27
+
28
+ ```bash
29
+ auvux login # GitHub Device Flow, caches a JWT in ~/.auvux/
30
+ auvux whoami # confirm
31
+ ```
32
+
33
+ ```python
34
+ import auvux
35
+ from auvux import lanes, markers
36
+
37
+ # Same as `auvux login`, but inline. Idempotent — no-op if already
38
+ # signed in.
39
+ auvux.login()
40
+
41
+ canvas = auvux.Canvas("Beat Detection")
42
+ canvas.add(
43
+ lanes.Waveform.from_path("demo.wav", size=160, label="mix"),
44
+ markers=[markers.vlines(beats, color="#FFFF00")],
45
+ )
46
+ canvas.add(lanes.Heatmap(spect, yaxis="mel", fmin=30, fmax=11000, size=256))
47
+ canvas.add(lanes.line(t, beat_logits, color="#4CAF50", label="beats", fill=True))
48
+ canvas.minimap(from_lane=0)
49
+ canvas.clicks(beats, freq=800)
50
+ canvas.show() # opens at https://inference.auvux.com
51
+ ```
52
+
53
+ Open `https://inference.auvux.com` in your browser **before** running
54
+ `canvas.show()` — the publisher waits up to 60 seconds for at least
55
+ one viewer to be present.
56
+
57
+ `Waveform.from_path()` accepts both local paths and `http(s)://` URLs;
58
+ remote files are fetched once into memory and reused without a second
59
+ round-trip on emit.
60
+
61
+ ## What gets sent
62
+
63
+ Each call to `canvas.show()` packages:
64
+
65
+ - A `manifest.json` describing the lanes and their layout
66
+ - Sidecar files: WAV / NPY / NPZ / CSV / JSON, per lane type
67
+ - Click-track time lists (if any)
68
+
69
+ The viewer pre-announces which file hashes it already has, so unchanged
70
+ audio across iterations is sent **once** — subsequent runs reuse the
71
+ cached blob.
72
+
73
+ ## Auth flow
74
+
75
+ `auvux.login()` runs the GitHub Device Flow with scope `user:email`:
76
+
77
+ 1. Asks GitHub for a short user code (`XXXX-XXXX`)
78
+ 2. Opens the browser to the verification URL
79
+ 3. Polls until you authorise
80
+ 4. Swaps the GitHub access token for an auvux JWT at
81
+ `https://inference.auvux.com/api/auth/cli-exchange`
82
+ 5. Caches the JWT in `~/.auvux/credentials.json` (chmod 600)
83
+
84
+ Subsequent `auvux.login()` calls are no-ops if the cached token is
85
+ still valid; pass `force=True` to re-authenticate.
86
+
87
+ ## Environment
88
+
89
+ | Var | Default | Purpose |
90
+ | --- | --- | --- |
91
+ | `AUVUX_SIGNALLING_URL` | from `~/.auvux/credentials.json`, otherwise `wss://signalling.auvux.com/signalling/me` | Override the signalling endpoint (escape hatch for local dev) |
92
+ | `AUVUX_API_BASE` | `https://inference.auvux.com` | Auvux API host used for `cli-exchange` |
93
+ | `AUVUX_GH_CLIENT_ID` | built-in | GitHub OAuth App client id |
94
+ | `AUVUX_PEER_WAIT` | `60` | Seconds to wait for a viewer before erroring |
95
+ | `AUVUX_ACK_WAIT` | `30` | Seconds to wait per-viewer for delivery ack |
96
+
97
+ ## Development
98
+
99
+ ```bash
100
+ git clone https://github.com/auvux/auvux-py
101
+ cd auvux-py
102
+ pip install -e .
103
+ ```
104
+
105
+ Release a new version: bump `version` in `pyproject.toml`, then
106
+
107
+ ```bash
108
+ git tag v0.1.0a2
109
+ git push --tags
110
+ ```
111
+
112
+ The `release.yml` workflow builds the wheel + sdist and publishes to
113
+ PyPI via OIDC (Trusted Publisher) — no API token in the repo.
114
+
115
+ ## License
116
+
117
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,30 @@
1
+ """auvux — push inference runs from a notebook to the browser viewer
2
+ over WebRTC. Implements the canvas-api-design.md spec.
3
+
4
+ import auvux
5
+ from auvux import lanes, markers
6
+
7
+ auvux.login() # idempotent: skips if already signed in
8
+
9
+ canvas = auvux.Canvas("title") # duration auto-derived
10
+ canvas.add(lanes.Waveform.from_path(audio_path),
11
+ markers=[markers.vlines(beats, color="#FF0")])
12
+ canvas.add(lanes.Heatmap(spect, yaxis="mel", fmin=30, fmax=11000))
13
+ canvas.add(lanes.line(t, y, color="#0AF", label="bpm"))
14
+ canvas.minimap(from_lane=0)
15
+ canvas.clicks(beats, freq=800)
16
+ canvas.show()
17
+ """
18
+
19
+ from .canvas import Canvas
20
+ from .auth import login, logout, whoami
21
+ from . import lanes
22
+ from . import markers
23
+ from . import plots
24
+
25
+ __version__ = "0.1.0a1"
26
+
27
+ __all__ = [
28
+ "Canvas", "login", "logout", "whoami",
29
+ "lanes", "markers", "plots", "__version__",
30
+ ]
@@ -0,0 +1,216 @@
1
+ """In-process auth — same Device Flow as `auvux login`, callable from
2
+ a notebook cell.
3
+
4
+ Typical use:
5
+
6
+ import auvux
7
+ auvux.login() # prints code, opens browser, polls
8
+ auvux.whoami() # confirm
9
+ canvas.show() # now picks up the cached JWT
10
+
11
+ The CLI's `auvux login` is a thin wrapper around `login()` here, so
12
+ behaviour stays identical across both entry points.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import os
19
+ import time
20
+ import urllib.error
21
+ import urllib.parse
22
+ import urllib.request
23
+ import webbrowser
24
+
25
+ from . import credentials
26
+
27
+
28
+ DEFAULT_SIGNALLING_URL = os.environ.get(
29
+ "AUVUX_SIGNALLING_URL", "wss://signalling.auvux.com/signalling/me"
30
+ )
31
+ DEFAULT_API_BASE = os.environ.get(
32
+ "AUVUX_API_BASE", "https://inference.auvux.com"
33
+ )
34
+ # Public OAuth App client_id — same one the browser flow uses. Safe to
35
+ # embed; the corresponding secret lives only on Vercel.
36
+ DEFAULT_GH_CLIENT_ID = os.environ.get(
37
+ "AUVUX_GH_CLIENT_ID", "Ov23liqKUtwhWWde5r73"
38
+ )
39
+
40
+
41
+ def login(
42
+ *,
43
+ signalling_url: str = DEFAULT_SIGNALLING_URL,
44
+ api_base: str = DEFAULT_API_BASE,
45
+ client_id: str = DEFAULT_GH_CLIENT_ID,
46
+ open_browser: bool = True,
47
+ force: bool = False,
48
+ ) -> credentials.Credentials:
49
+ """Run the GitHub Device Flow, swap the access_token for an auvux
50
+ JWT at /api/auth/cli-exchange, and cache it in
51
+ ~/.auvux/credentials.json. Returns the stored credentials dict.
52
+
53
+ Idempotent by default: if a valid cached JWT already exists, this
54
+ is a no-op (returns the cached credentials) and prints a one-line
55
+ confirmation. Pass `force=True` to throw away the cached token and
56
+ re-run the Device Flow.
57
+
58
+ Safe to call from inside Jupyter — prints the code inline and
59
+ opens the browser to the verification URL.
60
+ """
61
+ if not force:
62
+ existing = credentials.load()
63
+ if credentials.is_valid(existing) and existing is not None:
64
+ email = existing.get("email") or "<unknown>"
65
+ remaining = max(0, existing.get("expires_at", 0) - int(time.time()))
66
+ days = remaining // 86400
67
+ print(
68
+ f"auvux: already signed in as {email} "
69
+ f"(expires in {days}d). Pass force=True to re-authenticate."
70
+ )
71
+ return existing
72
+
73
+ device = _request_device_code(client_id)
74
+
75
+ print()
76
+ print(" ┌── auvux login ──────────────────────────────────────")
77
+ print(f" │ Go to: {device['verification_uri']}")
78
+ print(f" │ Code: {device['user_code']}")
79
+ print(" └─────────────────────────────────────────────────────")
80
+ print()
81
+
82
+ target = device.get("verification_uri_complete") or device["verification_uri"]
83
+ if open_browser:
84
+ try:
85
+ webbrowser.open(target)
86
+ print(f"auvux: opening {target} in your browser…")
87
+ except Exception:
88
+ pass
89
+
90
+ gh_token = _poll_for_token(
91
+ client_id,
92
+ device["device_code"],
93
+ interval=int(device.get("interval", 5)),
94
+ expires_in=int(device.get("expires_in", 900)),
95
+ )
96
+
97
+ auvux_resp = _exchange(api_base, gh_token)
98
+
99
+ creds: credentials.Credentials = {
100
+ "signalling_url": signalling_url,
101
+ "token": auvux_resp["token"],
102
+ "expires_at": (
103
+ int(time.time()) + int(auvux_resp["expires_in"])
104
+ if auvux_resp.get("expires_in")
105
+ else 0
106
+ ),
107
+ "email": auvux_resp.get("email", ""),
108
+ }
109
+ credentials.save(creds)
110
+ print(f"auvux: credentials saved → {credentials.credentials_path()}")
111
+ if creds.get("email"):
112
+ print(f"auvux: signed in as {creds['email']}")
113
+ return creds
114
+
115
+
116
+ def logout() -> None:
117
+ """Remove cached credentials. Does nothing if not logged in."""
118
+ credentials.clear()
119
+ print("auvux: credentials removed.")
120
+
121
+
122
+ def whoami() -> credentials.Credentials | None:
123
+ """Return (and print) the cached identity, or None if logged out."""
124
+ c = credentials.load()
125
+ if not c:
126
+ print("auvux: not logged in (call `auvux.login()`).")
127
+ return None
128
+ print(f"signalling: {c.get('signalling_url', '')}")
129
+ print(f"email: {c.get('email', '') or '<unknown>'}")
130
+ exp = c.get("expires_at", 0)
131
+ if exp:
132
+ remaining = max(0, exp - int(time.time()))
133
+ print(f"expires_in: {remaining}s")
134
+ return c
135
+
136
+
137
+ # ── GitHub Device Flow + cli-exchange (HTTP helpers) ──────────────────
138
+
139
+ def _request_device_code(client_id: str) -> dict:
140
+ """POST github.com/login/device/code → { device_code, user_code,
141
+ verification_uri, expires_in, interval }."""
142
+ return _post_form(
143
+ "https://github.com/login/device/code",
144
+ {"client_id": client_id, "scope": "user:email"},
145
+ )
146
+
147
+
148
+ def _poll_for_token(
149
+ client_id: str, device_code: str, interval: int, expires_in: int,
150
+ ) -> str:
151
+ """Poll the GitHub token endpoint until we get an access_token, a
152
+ fatal error, or the device code expires. Returns the access_token."""
153
+ deadline = time.time() + expires_in
154
+ sleep_s = max(interval, 1)
155
+ print(f"auvux: waiting for authorisation (poll every {sleep_s}s)…")
156
+ while time.time() < deadline:
157
+ time.sleep(sleep_s)
158
+ try:
159
+ body = _post_form(
160
+ "https://github.com/login/oauth/access_token",
161
+ {
162
+ "client_id": client_id,
163
+ "device_code": device_code,
164
+ "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
165
+ },
166
+ )
167
+ except urllib.error.HTTPError as e:
168
+ raise RuntimeError(f"GitHub token poll HTTP {e.code}") from e
169
+ if "access_token" in body:
170
+ return body["access_token"]
171
+ err = body.get("error")
172
+ if err == "authorization_pending":
173
+ continue
174
+ if err == "slow_down":
175
+ sleep_s += 5
176
+ continue
177
+ if err in {"expired_token", "access_denied"}:
178
+ raise RuntimeError(f"GitHub login failed: {err}")
179
+ raise RuntimeError(f"unexpected token response: {body}")
180
+ raise RuntimeError("login timed out — device code expired before authorisation")
181
+
182
+
183
+ def _exchange(api_base: str, gh_token: str) -> dict:
184
+ """Swap a GitHub access_token for an auvux JWT.
185
+
186
+ Cloudflare bot protection blocks Python's default
187
+ `User-Agent: Python-urllib/…`, so we send a recognisable UA string.
188
+ """
189
+ req = urllib.request.Request(
190
+ api_base.rstrip("/") + "/api/auth/cli-exchange",
191
+ data=json.dumps({"github_token": gh_token}).encode(),
192
+ headers={
193
+ "Content-Type": "application/json",
194
+ "Accept": "application/json",
195
+ "User-Agent": "auvux-cli/0.1 (+https://inference.auvux.com)",
196
+ },
197
+ method="POST",
198
+ )
199
+ with urllib.request.urlopen(req, timeout=30) as resp:
200
+ return json.loads(resp.read())
201
+
202
+
203
+ def _post_form(url: str, fields: dict) -> dict:
204
+ data = urllib.parse.urlencode(fields).encode()
205
+ req = urllib.request.Request(
206
+ url,
207
+ data=data,
208
+ headers={
209
+ "Accept": "application/json",
210
+ "Content-Type": "application/x-www-form-urlencoded",
211
+ "User-Agent": "auvux-cli",
212
+ },
213
+ method="POST",
214
+ )
215
+ with urllib.request.urlopen(req, timeout=30) as resp:
216
+ return json.loads(resp.read())