duhub-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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Digital Umuganda
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.
@@ -0,0 +1,193 @@
1
+ Metadata-Version: 2.4
2
+ Name: duhub-cli
3
+ Version: 0.1.0
4
+ Summary: DUhub CLI — STT, TTS, MT and diarization over your API key.
5
+ Author: Digital Umuganda
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://beta.duhub.ai
8
+ Project-URL: Repository, https://github.com/Digital-Umuganda/duhub-cli
9
+ Project-URL: Issues, https://github.com/Digital-Umuganda/duhub-cli/issues
10
+ Keywords: kinyarwanda,stt,tts,translation,diarization,speech
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
19
+ Requires-Python: >=3.11
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: typer>=0.9
23
+ Requires-Dist: rich>=13
24
+ Requires-Dist: httpx>=0.27
25
+ Dynamic: license-file
26
+
27
+ # duhub-cli
28
+
29
+ Command-line client for [DUhub](https://beta.duhub.ai):
30
+ transcribe audio, synthesize speech, translate text, and diarize meetings —
31
+ all billed to your API key.
32
+
33
+ ```bash
34
+ duhub stt rec.wav -o transcript.json
35
+ duhub tts "Muraho neza nshuti yanjye." -o out.wav
36
+ duhub mt "Muraho" --src rw --tgt en
37
+ duhub diarize meeting.wav --format srt -o speakers.srt
38
+ ```
39
+
40
+ ## Installation
41
+
42
+ Requires Python 3.11+.
43
+
44
+ ```bash
45
+ pipx install duhub-cli
46
+ duhub --version
47
+ ```
48
+
49
+ Alternatives:
50
+
51
+ ```bash
52
+ pipx install git+https://github.com/Digital-Umuganda/duhub-cli.git
53
+ pip install git+https://github.com/Digital-Umuganda/duhub-cli.git # plain pip
54
+ python -m du_cli.main --help # without installing
55
+ ```
56
+
57
+ > The installed command is `duhub` only, so it does not shadow the Unix
58
+ > `du` (disk usage). You can also invoke `python -m du_cli.main` without
59
+ > installing.
60
+
61
+ ## Quickstart
62
+
63
+ 1. Create an account on the
64
+ [Dashboard](https://beta.duhub.ai) and copy an
65
+ API key from **API Keys** (shown once — treat it like a password).
66
+ 2. Save it:
67
+ ```bash
68
+ duhub configure --api-key du_live_xxxxxxxxxxxxxxxx
69
+ ```
70
+ 3. Check connectivity:
71
+ ```bash
72
+ duhub health
73
+ duhub health --detailed
74
+ ```
75
+ 4. Transcribe something:
76
+ ```bash
77
+ duhub stt rec.wav
78
+ ```
79
+
80
+ ## Configuration
81
+
82
+ `duhub configure` stores settings in `~/.config/du/config.toml`:
83
+
84
+ | Source | Priority | Example |
85
+ |--------|----------|---------|
86
+ | Environment | Highest (always wins) | `DU_API_KEY`, `DU_BASE_URL` |
87
+ | Config file | Middle | `duhub configure --api-key … --base-url …` |
88
+ | Built-in default | Lowest | base `https://du-api.dev.umuganda.digital` |
89
+
90
+ ```bash
91
+ duhub configure # show current state
92
+ duhub configure --api-key du_live_xxx # set key
93
+ duhub configure --base-url http://localhost:8080 # point at local gateway
94
+ duhub configure --reset # delete the file
95
+ export DU_API_KEY=du_live_xxx DU_BASE_URL=https://du-api.dev.umuganda.digital
96
+ ```
97
+
98
+ > Prefer `DU_API_KEY` env var over `--api-key` — flags land in shell history.
99
+
100
+ ## Commands
101
+
102
+ Every command exits `0` on success, `1` on failure (`2` on CLI usage
103
+ errors) and prints a summary line (`units`, `cache`, `request-id`). The
104
+ API commands (`health`, `stt`, `tts`, `mt`, `diarize`) accept `--json`
105
+ for a single machine-readable object on stdout
106
+ (`{"data", "units", "cache", "request_id"}`). `-o` writes the raw
107
+ payload to a file instead of stdout.
108
+
109
+ Every billed command (`stt`, `tts`, `mt`, `diarize`) accepts
110
+ `--idempotency-key`, so a retry after a timeout or connection drop is
111
+ deduplicated by the gateway instead of being billed twice.
112
+
113
+ ### `duhub health`
114
+
115
+ Liveness (`/health`) or full readiness (`--detailed`, checks STT/TTS/MT
116
+ backends — the one to try first when calls fail).
117
+
118
+ ### `duhub stt` — speech to text
119
+
120
+ Upload audio (WAV/FLAC/OGG, ≤ 30 MB), get `{"text": "…"}`.
121
+ **Billed per audio second.**
122
+
123
+ ```bash
124
+ duhub stt rec.wav -o transcript.json
125
+ duhub stt rec.wav --idempotency-key run-42 -o transcript.json
126
+ ```
127
+
128
+ ### `duhub tts` — text to speech
129
+
130
+ Synthesize Kinyarwanda speech to a 24 kHz mono WAV (`-o` is required —
131
+ audio never goes to stdout). **Billed per word.** `--text-file`
132
+ overrides `TEXT` when both are given.
133
+
134
+ ```bash
135
+ duhub tts "Muraho neza nshuti yanjye." -o out.wav
136
+ duhub tts "Muraho neza nshuti yanjye." -o out.wav --idempotency-key run-42
137
+ ```
138
+
139
+ ### `duhub mt` — translation
140
+
141
+ Translate between Kinyarwanda (`rw`) and English (`en`).
142
+ **Billed per input word.**
143
+
144
+ ```bash
145
+ duhub mt "Muraho" --src rw --tgt en
146
+ ```
147
+
148
+ ### `duhub diarize` — who spoke when
149
+
150
+ Upload audio, get speaker-labelled segments. `--format` renders the same
151
+ result as `json` (default), `txt`, or `srt`. **Billed per audio second.**
152
+
153
+ ```bash
154
+ duhub diarize meeting.wav --format srt -o speakers.srt
155
+ ```
156
+
157
+ > A `404` here usually means the configured gateway predates the
158
+ > `/diarize` route. Check `duhub health --detailed` and your `--base-url`.
159
+
160
+ ## Scripting
161
+
162
+ `--json` prints exactly one JSON object — pipe-safe:
163
+
164
+ ```bash
165
+ TEXT=$(duhub stt rec.wav --json | jq -r .data.text)
166
+
167
+ for f in clips/*.wav; do
168
+ duhub stt "$f" -o "${f%.wav}.json" || echo "FAILED: $f" >&2
169
+ done
170
+ ```
171
+
172
+ ## Errors
173
+
174
+ API, input-file, and output-file failures are reported as `error` +
175
+ `hint` — no tracebacks:
176
+
177
+ | You see | Cause | Fix |
178
+ |---------|-------|-----|
179
+ | `invalid key` (401) | Missing/unknown/revoked key | Copy a fresh key from dashboard API Keys |
180
+ | `insufficient balance` (402) | Wallet empty | Top-up via dashboard Wallet |
181
+ | `organization pending review` (403) | Org not approved yet | Contact your org admin |
182
+ | `file exceeds 30 MB` | Upload guard | Compress or split the audio |
183
+ | `server busy (saturated)` (503) | GPU at concurrency cap | Wait `Retry-After` seconds, retry |
184
+ | `backend unreachable` (502) | Model container down | `duhub health --detailed` to see which |
185
+ | `cannot reach server` | Wrong `--base-url`, offline, timeout | Check URL and connectivity |
186
+
187
+ ## Development
188
+
189
+ ```bash
190
+ uv sync # or: python -m venv .venv && .venv/bin/pip install -e .
191
+ uv run pytest
192
+ uv run duhub --help
193
+ ```
@@ -0,0 +1,167 @@
1
+ # duhub-cli
2
+
3
+ Command-line client for [DUhub](https://beta.duhub.ai):
4
+ transcribe audio, synthesize speech, translate text, and diarize meetings —
5
+ all billed to your API key.
6
+
7
+ ```bash
8
+ duhub stt rec.wav -o transcript.json
9
+ duhub tts "Muraho neza nshuti yanjye." -o out.wav
10
+ duhub mt "Muraho" --src rw --tgt en
11
+ duhub diarize meeting.wav --format srt -o speakers.srt
12
+ ```
13
+
14
+ ## Installation
15
+
16
+ Requires Python 3.11+.
17
+
18
+ ```bash
19
+ pipx install duhub-cli
20
+ duhub --version
21
+ ```
22
+
23
+ Alternatives:
24
+
25
+ ```bash
26
+ pipx install git+https://github.com/Digital-Umuganda/duhub-cli.git
27
+ pip install git+https://github.com/Digital-Umuganda/duhub-cli.git # plain pip
28
+ python -m du_cli.main --help # without installing
29
+ ```
30
+
31
+ > The installed command is `duhub` only, so it does not shadow the Unix
32
+ > `du` (disk usage). You can also invoke `python -m du_cli.main` without
33
+ > installing.
34
+
35
+ ## Quickstart
36
+
37
+ 1. Create an account on the
38
+ [Dashboard](https://beta.duhub.ai) and copy an
39
+ API key from **API Keys** (shown once — treat it like a password).
40
+ 2. Save it:
41
+ ```bash
42
+ duhub configure --api-key du_live_xxxxxxxxxxxxxxxx
43
+ ```
44
+ 3. Check connectivity:
45
+ ```bash
46
+ duhub health
47
+ duhub health --detailed
48
+ ```
49
+ 4. Transcribe something:
50
+ ```bash
51
+ duhub stt rec.wav
52
+ ```
53
+
54
+ ## Configuration
55
+
56
+ `duhub configure` stores settings in `~/.config/du/config.toml`:
57
+
58
+ | Source | Priority | Example |
59
+ |--------|----------|---------|
60
+ | Environment | Highest (always wins) | `DU_API_KEY`, `DU_BASE_URL` |
61
+ | Config file | Middle | `duhub configure --api-key … --base-url …` |
62
+ | Built-in default | Lowest | base `https://du-api.dev.umuganda.digital` |
63
+
64
+ ```bash
65
+ duhub configure # show current state
66
+ duhub configure --api-key du_live_xxx # set key
67
+ duhub configure --base-url http://localhost:8080 # point at local gateway
68
+ duhub configure --reset # delete the file
69
+ export DU_API_KEY=du_live_xxx DU_BASE_URL=https://du-api.dev.umuganda.digital
70
+ ```
71
+
72
+ > Prefer `DU_API_KEY` env var over `--api-key` — flags land in shell history.
73
+
74
+ ## Commands
75
+
76
+ Every command exits `0` on success, `1` on failure (`2` on CLI usage
77
+ errors) and prints a summary line (`units`, `cache`, `request-id`). The
78
+ API commands (`health`, `stt`, `tts`, `mt`, `diarize`) accept `--json`
79
+ for a single machine-readable object on stdout
80
+ (`{"data", "units", "cache", "request_id"}`). `-o` writes the raw
81
+ payload to a file instead of stdout.
82
+
83
+ Every billed command (`stt`, `tts`, `mt`, `diarize`) accepts
84
+ `--idempotency-key`, so a retry after a timeout or connection drop is
85
+ deduplicated by the gateway instead of being billed twice.
86
+
87
+ ### `duhub health`
88
+
89
+ Liveness (`/health`) or full readiness (`--detailed`, checks STT/TTS/MT
90
+ backends — the one to try first when calls fail).
91
+
92
+ ### `duhub stt` — speech to text
93
+
94
+ Upload audio (WAV/FLAC/OGG, ≤ 30 MB), get `{"text": "…"}`.
95
+ **Billed per audio second.**
96
+
97
+ ```bash
98
+ duhub stt rec.wav -o transcript.json
99
+ duhub stt rec.wav --idempotency-key run-42 -o transcript.json
100
+ ```
101
+
102
+ ### `duhub tts` — text to speech
103
+
104
+ Synthesize Kinyarwanda speech to a 24 kHz mono WAV (`-o` is required —
105
+ audio never goes to stdout). **Billed per word.** `--text-file`
106
+ overrides `TEXT` when both are given.
107
+
108
+ ```bash
109
+ duhub tts "Muraho neza nshuti yanjye." -o out.wav
110
+ duhub tts "Muraho neza nshuti yanjye." -o out.wav --idempotency-key run-42
111
+ ```
112
+
113
+ ### `duhub mt` — translation
114
+
115
+ Translate between Kinyarwanda (`rw`) and English (`en`).
116
+ **Billed per input word.**
117
+
118
+ ```bash
119
+ duhub mt "Muraho" --src rw --tgt en
120
+ ```
121
+
122
+ ### `duhub diarize` — who spoke when
123
+
124
+ Upload audio, get speaker-labelled segments. `--format` renders the same
125
+ result as `json` (default), `txt`, or `srt`. **Billed per audio second.**
126
+
127
+ ```bash
128
+ duhub diarize meeting.wav --format srt -o speakers.srt
129
+ ```
130
+
131
+ > A `404` here usually means the configured gateway predates the
132
+ > `/diarize` route. Check `duhub health --detailed` and your `--base-url`.
133
+
134
+ ## Scripting
135
+
136
+ `--json` prints exactly one JSON object — pipe-safe:
137
+
138
+ ```bash
139
+ TEXT=$(duhub stt rec.wav --json | jq -r .data.text)
140
+
141
+ for f in clips/*.wav; do
142
+ duhub stt "$f" -o "${f%.wav}.json" || echo "FAILED: $f" >&2
143
+ done
144
+ ```
145
+
146
+ ## Errors
147
+
148
+ API, input-file, and output-file failures are reported as `error` +
149
+ `hint` — no tracebacks:
150
+
151
+ | You see | Cause | Fix |
152
+ |---------|-------|-----|
153
+ | `invalid key` (401) | Missing/unknown/revoked key | Copy a fresh key from dashboard API Keys |
154
+ | `insufficient balance` (402) | Wallet empty | Top-up via dashboard Wallet |
155
+ | `organization pending review` (403) | Org not approved yet | Contact your org admin |
156
+ | `file exceeds 30 MB` | Upload guard | Compress or split the audio |
157
+ | `server busy (saturated)` (503) | GPU at concurrency cap | Wait `Retry-After` seconds, retry |
158
+ | `backend unreachable` (502) | Model container down | `duhub health --detailed` to see which |
159
+ | `cannot reach server` | Wrong `--base-url`, offline, timeout | Check URL and connectivity |
160
+
161
+ ## Development
162
+
163
+ ```bash
164
+ uv sync # or: python -m venv .venv && .venv/bin/pip install -e .
165
+ uv run pytest
166
+ uv run duhub --help
167
+ ```
@@ -0,0 +1 @@
1
+ """duhub CLI — external developer client for DUhub."""
@@ -0,0 +1,65 @@
1
+ """Single httpx wrapper: auth headers, error hints, timeout."""
2
+ from __future__ import annotations
3
+
4
+ import contextlib
5
+
6
+ import httpx
7
+
8
+ TIMEOUT = 300.0
9
+ CONNECT_TIMEOUT = 10.0
10
+ MAX_UPLOAD_BYTES = 30 * 1024 * 1024
11
+
12
+ class CliError(Exception):
13
+ def __init__(self, status: int, detail: str, hint: str):
14
+ super().__init__(detail)
15
+ self.status = status
16
+ self.detail = detail
17
+ self.hint = hint
18
+
19
+ def make_client() -> httpx.Client:
20
+ # Long read timeout for audio jobs, short connect timeout for bad hosts.
21
+ return httpx.Client(timeout=httpx.Timeout(TIMEOUT, connect=CONNECT_TIMEOUT))
22
+
23
+ @contextlib.contextmanager
24
+ def api_client():
25
+ """Yield an httpx client; transport failures become CliError (no tracebacks)."""
26
+ try:
27
+ with make_client() as c:
28
+ yield c
29
+ except httpx.RequestError as e:
30
+ raise CliError(
31
+ 502,
32
+ f"cannot reach server: {e}",
33
+ "check base URL and try `duhub health --detailed`",
34
+ ) from e
35
+
36
+ def auth_headers(api_key: str, idempotency_key: str | None = None) -> dict[str, str]:
37
+ h = {"X-API-Key": api_key}
38
+ if idempotency_key:
39
+ h["Idempotency-Key"] = idempotency_key
40
+ return h
41
+
42
+ def error_hint(status: int, detail: str, path: str = "") -> str:
43
+ d = (detail or "").lower()
44
+ if status == 401:
45
+ return "invalid key — run `duhub configure`, copy from dashboard API Keys"
46
+ if status == 402 or "insufficient_balance" in d:
47
+ return "insufficient balance — top-up via dashboard Wallet"
48
+ if status == 403:
49
+ return "organization pending review — contact org admin"
50
+ if status == 413:
51
+ return "file exceeds 30 MB limit — compress or split audio"
52
+ if status == 503:
53
+ return "server busy (saturated) — retry after Retry-After seconds"
54
+ if status == 502:
55
+ return "backend unreachable — try `duhub health --detailed`"
56
+ if status == 404:
57
+ return "not found — check base URL and gateway version"
58
+ return detail or f"HTTP {status}"
59
+
60
+ def raise_for_api(status: int, body: str, path: str = "") -> None:
61
+ if 200 <= status < 300:
62
+ return
63
+ detail = body[:500] if body else f"HTTP {status}"
64
+ # try to keep gateway JSON detail readable
65
+ raise CliError(status, detail, error_hint(status, detail, path))
@@ -0,0 +1,67 @@
1
+ """Config load/save: env > ~/.config/du/config.toml > defaults."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ import tomllib
6
+ from pathlib import Path
7
+
8
+ DEFAULT_BASE_URL = "https://du-api.dev.umuganda.digital"
9
+ CONFIG_PATH = Path.home() / ".config" / "du" / "config.toml"
10
+
11
+ def _read_file(path: Path) -> dict[str, str]:
12
+ try:
13
+ with path.open("rb") as fh:
14
+ data = tomllib.load(fh)
15
+ except (FileNotFoundError, tomllib.TOMLDecodeError, UnicodeDecodeError, OSError):
16
+ return {}
17
+ out: dict[str, str] = {}
18
+ if isinstance(data.get("base_url"), str):
19
+ out["base_url"] = data["base_url"]
20
+ if isinstance(data.get("api_key"), str):
21
+ out["api_key"] = data["api_key"]
22
+ return out
23
+
24
+ def load_config(base_url_override: str | None = None, api_key_override: str | None = None) -> dict[str, str]:
25
+ filed = _read_file(CONFIG_PATH)
26
+ base = base_url_override if base_url_override is not None else os.environ.get("DU_BASE_URL", filed.get("base_url", DEFAULT_BASE_URL))
27
+ key = api_key_override if api_key_override is not None else os.environ.get("DU_API_KEY", filed.get("api_key", ""))
28
+ return {"base_url": base.strip().rstrip("/"), "api_key": key.strip()}
29
+
30
+ def _q(v: str) -> str:
31
+ escaped = (
32
+ v.replace("\\", "\\\\")
33
+ .replace('"', '\\"')
34
+ .replace("\n", "\\n")
35
+ .replace("\r", "\\r")
36
+ .replace("\t", "\\t")
37
+ )
38
+ return '"' + escaped + '"'
39
+
40
+ def save_config(base_url: str | None, api_key: str | None) -> Path:
41
+ CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
42
+ prev = _read_file(CONFIG_PATH)
43
+ if base_url is not None:
44
+ prev["base_url"] = base_url.strip().rstrip("/")
45
+ if api_key is not None:
46
+ prev["api_key"] = api_key.strip()
47
+ lines = []
48
+ if "base_url" in prev:
49
+ lines.append(f"base_url = {_q(prev['base_url'])}")
50
+ if "api_key" in prev:
51
+ lines.append(f"api_key = {_q(prev['api_key'])}")
52
+ text = "\n".join(lines) + ("\n" if lines else "")
53
+ fd = os.open(CONFIG_PATH, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
54
+ if hasattr(os, "fchmod"):
55
+ try:
56
+ os.fchmod(fd, 0o600) # tighten before the key is written
57
+ except OSError:
58
+ pass
59
+ with os.fdopen(fd, "w", encoding="utf-8") as fh:
60
+ fh.write(text)
61
+ return CONFIG_PATH
62
+
63
+ def clear_config() -> None:
64
+ try:
65
+ CONFIG_PATH.unlink()
66
+ except FileNotFoundError:
67
+ pass