vezir 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.
Files changed (40) hide show
  1. vezir-0.1.0/LICENSE +21 -0
  2. vezir-0.1.0/PKG-INFO +216 -0
  3. vezir-0.1.0/README.md +173 -0
  4. vezir-0.1.0/pyproject.toml +74 -0
  5. vezir-0.1.0/setup.cfg +4 -0
  6. vezir-0.1.0/tests/test_auth.py +180 -0
  7. vezir-0.1.0/tests/test_queue.py +48 -0
  8. vezir-0.1.0/vezir/__init__.py +3 -0
  9. vezir-0.1.0/vezir/cli.py +184 -0
  10. vezir-0.1.0/vezir/client/__init__.py +0 -0
  11. vezir-0.1.0/vezir/client/gui.py +562 -0
  12. vezir-0.1.0/vezir/client/scribe.py +130 -0
  13. vezir-0.1.0/vezir/client/uploader.py +65 -0
  14. vezir-0.1.0/vezir/config.py +93 -0
  15. vezir-0.1.0/vezir/server/__init__.py +0 -0
  16. vezir-0.1.0/vezir/server/app.py +64 -0
  17. vezir-0.1.0/vezir/server/auth.py +163 -0
  18. vezir-0.1.0/vezir/server/labels.py +216 -0
  19. vezir-0.1.0/vezir/server/login.py +119 -0
  20. vezir-0.1.0/vezir/server/meet_runner.py +278 -0
  21. vezir-0.1.0/vezir/server/queue.py +153 -0
  22. vezir-0.1.0/vezir/server/sessions.py +97 -0
  23. vezir-0.1.0/vezir/server/templating.py +9 -0
  24. vezir-0.1.0/vezir/server/uploads.py +105 -0
  25. vezir-0.1.0/vezir/server/voiceprints.py +59 -0
  26. vezir-0.1.0/vezir/server/worker.py +363 -0
  27. vezir-0.1.0/vezir/web/__init__.py +0 -0
  28. vezir-0.1.0/vezir/web/static/style.css +175 -0
  29. vezir-0.1.0/vezir/web/templates/base.html +20 -0
  30. vezir-0.1.0/vezir/web/templates/dashboard.html +39 -0
  31. vezir-0.1.0/vezir/web/templates/label.html +56 -0
  32. vezir-0.1.0/vezir/web/templates/label_pending.html +10 -0
  33. vezir-0.1.0/vezir/web/templates/login.html +28 -0
  34. vezir-0.1.0/vezir/web/templates/session.html +32 -0
  35. vezir-0.1.0/vezir.egg-info/PKG-INFO +216 -0
  36. vezir-0.1.0/vezir.egg-info/SOURCES.txt +38 -0
  37. vezir-0.1.0/vezir.egg-info/dependency_links.txt +1 -0
  38. vezir-0.1.0/vezir.egg-info/entry_points.txt +2 -0
  39. vezir-0.1.0/vezir.egg-info/requires.txt +18 -0
  40. vezir-0.1.0/vezir.egg-info/top_level.txt +1 -0
vezir-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Blink
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.
vezir-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,216 @@
1
+ Metadata-Version: 2.4
2
+ Name: vezir
3
+ Version: 0.1.0
4
+ Summary: Internal scribe service that wraps meetscribe for team-scale meeting capture, transcription, summarization, and speaker labeling
5
+ Author: Blink
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/pretyflaco/vezir
8
+ Project-URL: Repository, https://github.com/pretyflaco/vezir
9
+ Project-URL: Issues, https://github.com/pretyflaco/vezir/issues
10
+ Keywords: meeting,transcription,scribe,team,self-hosted
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Environment :: Web Environment
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: POSIX :: Linux
17
+ Classifier: Operating System :: MacOS
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
23
+ Classifier: Topic :: Office/Business
24
+ Requires-Python: >=3.10
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: click>=8.0
28
+ Requires-Dist: httpx>=0.27
29
+ Requires-Dist: ulid-py>=1.1
30
+ Requires-Dist: meetscribe-record>=0.1.0
31
+ Provides-Extra: server
32
+ Requires-Dist: fastapi>=0.110; extra == "server"
33
+ Requires-Dist: uvicorn[standard]>=0.27; extra == "server"
34
+ Requires-Dist: python-multipart>=0.0.9; extra == "server"
35
+ Requires-Dist: jinja2>=3.1; extra == "server"
36
+ Requires-Dist: meetscribe-offline>=0.5.0; extra == "server"
37
+ Provides-Extra: gui
38
+ Provides-Extra: dev
39
+ Requires-Dist: ruff; extra == "dev"
40
+ Requires-Dist: pytest; extra == "dev"
41
+ Requires-Dist: pytest-asyncio; extra == "dev"
42
+ Dynamic: license-file
43
+
44
+ # vezir
45
+
46
+ Self-hosted scribe service for team-scale meeting capture. Vezir wraps
47
+ [meetscribe](https://github.com/pretyflaco/meetscribe) and turns it into a
48
+ multi-user, Tailscale-hosted service: a designated scribe records a meeting
49
+ on their laptop, the audio uploads to a central GPU-equipped box, and the
50
+ team gets back a diarized transcript, AI summary, and PDF — with speaker
51
+ labels resolved to GitHub handles via a shared web UI.
52
+
53
+ ## Status
54
+
55
+ Alpha (0.1.0). Designed for small teams that want to keep meeting audio
56
+ inside their own infrastructure: one Tailscale tailnet + one GPU-equipped
57
+ box. Currently dogfooded by the Blink team. Linux clients fully supported,
58
+ macOS thin client deferred.
59
+
60
+ ## Architecture
61
+
62
+ ```
63
+ [Scribe laptop] [GPU server]
64
+ vezir scribe / gui ──upload──▶ vezir serve (FastAPI)
65
+ (wraps meet record) │
66
+ ├── sqlite job queue
67
+
68
+
69
+ worker
70
+ │ shells out via HOME-shim
71
+
72
+ meet transcribe (unmodified)
73
+ meet label --auto
74
+ meet sync ──▶ private git repo
75
+
76
+
77
+ web UI (labeling, dashboard)
78
+ ◀── scribe browser
79
+ ```
80
+
81
+ Meetscribe is invoked as an unmodified subprocess. Vezir owns its own
82
+ job queue, voiceprint database, team roster, and browser auth.
83
+
84
+ ## Repo layout
85
+
86
+ ```
87
+ vezir/
88
+ vezir/ # python package
89
+ cli.py # serve, scribe, token issue
90
+ config.py # paths, env
91
+ server/ # FastAPI app, queue, worker, meet_runner
92
+ client/ # vezir scribe (wraps meet record + uploads)
93
+ web/ # templates + static
94
+ data/
95
+ team.json.example
96
+ infra/
97
+ systemd/vezir.service
98
+ tests/
99
+ ```
100
+
101
+ Runtime data lives **outside** the repo at `~/vezir-data/`.
102
+
103
+ ## Install profiles
104
+
105
+ | Role | Install command | Footprint |
106
+ |---|---|---|
107
+ | **Scribe client only** (record + upload, GUI optional) | `pip install --user vezir` (or `pip install --user 'vezir[gui]'` if you also want `apt install python3-tk`) | ~30 MB |
108
+ | **Server** (FastAPI + worker + dashboard + labeling UI) | `pip install --user 'vezir[server]'` | ~3 GB (pulls meetscribe-offline = whisperx + torch + pyannote) |
109
+
110
+ The split is enforced by `pyproject.toml`'s `[project.optional-dependencies]`:
111
+ the base install uses [meetscribe-record](https://github.com/pretyflaco/meetscribe-record)
112
+ (capture only). The `[server]` extra adds [meetscribe-offline](https://github.com/pretyflaco/meetscribe)
113
+ for the heavy transcription/diarization/summarization pipeline.
114
+
115
+ ## Quick start (server, on a GPU box reachable over Tailscale)
116
+
117
+ ```bash
118
+ git clone https://github.com/pretyflaco/vezir.git
119
+ cd vezir
120
+ pip install --user -e '.[server]'
121
+
122
+ # Seed voiceprints from existing meetscribe profile DB
123
+ mkdir -p ~/vezir-data
124
+ vezir voiceprints seed --from ~/.config/meet/speaker_profiles.json
125
+
126
+ # Sync target — sandbox repo for development.
127
+ # vezir's worker invokes `meet sync --force --meeting-type sandbox-<HHMMSSZ>-<rand>`
128
+ # which bypasses meetscribe's schedule and team-presence gates and
129
+ # guarantees a unique per-session folder. Every successful job lands in
130
+ # meetings/<date>_sandbox-<HHMMSSZ>-<rand>/ on the configured repo
131
+ # (e.g. meetings/2026-04-25_sandbox-194051Z-VZJJ3P/).
132
+ cat > ~/vezir-data/sync_config.json <<'EOF'
133
+ {
134
+ "repo_url": "https://github.com/pretyflaco/vezir-meetings.git",
135
+ "meetings": [],
136
+ "team_members": [],
137
+ "min_team_members": 0
138
+ }
139
+ EOF
140
+
141
+ # Initialize team roster (used by labeling UI autocomplete)
142
+ cp data/team.json.example ~/vezir-data/team.json
143
+ $EDITOR ~/vezir-data/team.json
144
+
145
+ # Issue a token for yourself
146
+ vezir token issue --github kasita
147
+
148
+ # Start the service
149
+ vezir serve
150
+
151
+ # Or, to skip git sync (artifacts stay only in ~/vezir-data/sessions/<id>/)
152
+ VEZIR_SKIP_SYNC=1 vezir serve
153
+ ```
154
+
155
+ ### Sync target governance
156
+
157
+ This is intentionally pointed at a private **dev sandbox** repo
158
+ (`pretyflaco/vezir-meetings`) during the pilot. Two reasons:
159
+
160
+ - production meeting-archive repos (e.g. `blinkbitcoin/blink-wip`) get
161
+ schedule + team-presence gating from meetscribe; vezir uses `--force`
162
+ to override that, which is appropriate for a dev sandbox but not for
163
+ production
164
+ - vezir may rewrite history or recreate the repo while the pipeline is
165
+ being shaken down
166
+
167
+ To graduate to production: change `repo_url` in
168
+ `~/vezir-data/sync_config.json`, drop `--force` (planned: env var
169
+ `VEZIR_SYNC_FORCE=0`), and let meetscribe's existing schedule/team-gate
170
+ decide what to push.
171
+
172
+ ## Quick start (scribe client)
173
+
174
+ ```bash
175
+ # Install vezir + meetscribe-record (lightweight; ~30 MB).
176
+ pip install --user vezir
177
+
178
+ # Optional: GUI widget (Tkinter); on Debian/Ubuntu:
179
+ sudo apt install python3-tk
180
+
181
+ # Configure (one-time): server URL = Tailscale name of your vezir server
182
+ export VEZIR_URL=http://your-vezir-server:8000
183
+ export VEZIR_TOKEN=<token-issued-on-server>
184
+
185
+ # CLI scribe
186
+ vezir scribe --title "what this meeting is about"
187
+ # Talk; Ctrl+C when done.
188
+
189
+ # Or GUI scribe (always-on-top widget)
190
+ vezir gui
191
+ ```
192
+
193
+ When the recording is uploaded, vezir prints a dashboard URL. Open it in
194
+ your browser; the GUI's "Open dashboard" button does this for you. The
195
+ URL flows through `/login?token=...` so the browser is signed in via
196
+ HttpOnly cookie before it lands on the session page; subsequent access
197
+ from the same browser does not require re-passing the token.
198
+
199
+ ## Environment variables
200
+
201
+ | Variable | Default | Effect |
202
+ |---|---|---|
203
+ | `VEZIR_DATA` | `~/vezir-data` | All runtime state — sessions, voiceprints, queue, tokens, sync_config |
204
+ | `VEZIR_HOST` | `0.0.0.0` | Bind address for `vezir serve` |
205
+ | `VEZIR_PORT` | `8000` | Port for `vezir serve` |
206
+ | `VEZIR_URL` | `http://localhost:8000` | Server URL for `vezir scribe` clients |
207
+ | `VEZIR_TOKEN` | — | Bearer token for `vezir scribe` clients |
208
+ | `VEZIR_LOG_LEVEL` | `INFO` | Logging level |
209
+ | `VEZIR_MEET_BIN` | `$(which meet)` | Path to meetscribe `meet` binary |
210
+ | `VEZIR_SKIP_SYNC` | unset | Set to `1` to skip the `meet sync` step entirely |
211
+ | `VEZIR_DELETE_AUDIO` | unset | Set to `1` to delete audio after artifacts are produced (storage policy). Default OFF during pilot. |
212
+ | `VEZIR_SYNC_MEETING_TYPE` | `sandbox` | Subfolder name (under `meetings/`) used by `meet sync --force`. Will be removed once vezir respects schedules. |
213
+
214
+ ## License
215
+
216
+ MIT — see [LICENSE](LICENSE).
vezir-0.1.0/README.md ADDED
@@ -0,0 +1,173 @@
1
+ # vezir
2
+
3
+ Self-hosted scribe service for team-scale meeting capture. Vezir wraps
4
+ [meetscribe](https://github.com/pretyflaco/meetscribe) and turns it into a
5
+ multi-user, Tailscale-hosted service: a designated scribe records a meeting
6
+ on their laptop, the audio uploads to a central GPU-equipped box, and the
7
+ team gets back a diarized transcript, AI summary, and PDF — with speaker
8
+ labels resolved to GitHub handles via a shared web UI.
9
+
10
+ ## Status
11
+
12
+ Alpha (0.1.0). Designed for small teams that want to keep meeting audio
13
+ inside their own infrastructure: one Tailscale tailnet + one GPU-equipped
14
+ box. Currently dogfooded by the Blink team. Linux clients fully supported,
15
+ macOS thin client deferred.
16
+
17
+ ## Architecture
18
+
19
+ ```
20
+ [Scribe laptop] [GPU server]
21
+ vezir scribe / gui ──upload──▶ vezir serve (FastAPI)
22
+ (wraps meet record) │
23
+ ├── sqlite job queue
24
+
25
+
26
+ worker
27
+ │ shells out via HOME-shim
28
+
29
+ meet transcribe (unmodified)
30
+ meet label --auto
31
+ meet sync ──▶ private git repo
32
+
33
+
34
+ web UI (labeling, dashboard)
35
+ ◀── scribe browser
36
+ ```
37
+
38
+ Meetscribe is invoked as an unmodified subprocess. Vezir owns its own
39
+ job queue, voiceprint database, team roster, and browser auth.
40
+
41
+ ## Repo layout
42
+
43
+ ```
44
+ vezir/
45
+ vezir/ # python package
46
+ cli.py # serve, scribe, token issue
47
+ config.py # paths, env
48
+ server/ # FastAPI app, queue, worker, meet_runner
49
+ client/ # vezir scribe (wraps meet record + uploads)
50
+ web/ # templates + static
51
+ data/
52
+ team.json.example
53
+ infra/
54
+ systemd/vezir.service
55
+ tests/
56
+ ```
57
+
58
+ Runtime data lives **outside** the repo at `~/vezir-data/`.
59
+
60
+ ## Install profiles
61
+
62
+ | Role | Install command | Footprint |
63
+ |---|---|---|
64
+ | **Scribe client only** (record + upload, GUI optional) | `pip install --user vezir` (or `pip install --user 'vezir[gui]'` if you also want `apt install python3-tk`) | ~30 MB |
65
+ | **Server** (FastAPI + worker + dashboard + labeling UI) | `pip install --user 'vezir[server]'` | ~3 GB (pulls meetscribe-offline = whisperx + torch + pyannote) |
66
+
67
+ The split is enforced by `pyproject.toml`'s `[project.optional-dependencies]`:
68
+ the base install uses [meetscribe-record](https://github.com/pretyflaco/meetscribe-record)
69
+ (capture only). The `[server]` extra adds [meetscribe-offline](https://github.com/pretyflaco/meetscribe)
70
+ for the heavy transcription/diarization/summarization pipeline.
71
+
72
+ ## Quick start (server, on a GPU box reachable over Tailscale)
73
+
74
+ ```bash
75
+ git clone https://github.com/pretyflaco/vezir.git
76
+ cd vezir
77
+ pip install --user -e '.[server]'
78
+
79
+ # Seed voiceprints from existing meetscribe profile DB
80
+ mkdir -p ~/vezir-data
81
+ vezir voiceprints seed --from ~/.config/meet/speaker_profiles.json
82
+
83
+ # Sync target — sandbox repo for development.
84
+ # vezir's worker invokes `meet sync --force --meeting-type sandbox-<HHMMSSZ>-<rand>`
85
+ # which bypasses meetscribe's schedule and team-presence gates and
86
+ # guarantees a unique per-session folder. Every successful job lands in
87
+ # meetings/<date>_sandbox-<HHMMSSZ>-<rand>/ on the configured repo
88
+ # (e.g. meetings/2026-04-25_sandbox-194051Z-VZJJ3P/).
89
+ cat > ~/vezir-data/sync_config.json <<'EOF'
90
+ {
91
+ "repo_url": "https://github.com/pretyflaco/vezir-meetings.git",
92
+ "meetings": [],
93
+ "team_members": [],
94
+ "min_team_members": 0
95
+ }
96
+ EOF
97
+
98
+ # Initialize team roster (used by labeling UI autocomplete)
99
+ cp data/team.json.example ~/vezir-data/team.json
100
+ $EDITOR ~/vezir-data/team.json
101
+
102
+ # Issue a token for yourself
103
+ vezir token issue --github kasita
104
+
105
+ # Start the service
106
+ vezir serve
107
+
108
+ # Or, to skip git sync (artifacts stay only in ~/vezir-data/sessions/<id>/)
109
+ VEZIR_SKIP_SYNC=1 vezir serve
110
+ ```
111
+
112
+ ### Sync target governance
113
+
114
+ This is intentionally pointed at a private **dev sandbox** repo
115
+ (`pretyflaco/vezir-meetings`) during the pilot. Two reasons:
116
+
117
+ - production meeting-archive repos (e.g. `blinkbitcoin/blink-wip`) get
118
+ schedule + team-presence gating from meetscribe; vezir uses `--force`
119
+ to override that, which is appropriate for a dev sandbox but not for
120
+ production
121
+ - vezir may rewrite history or recreate the repo while the pipeline is
122
+ being shaken down
123
+
124
+ To graduate to production: change `repo_url` in
125
+ `~/vezir-data/sync_config.json`, drop `--force` (planned: env var
126
+ `VEZIR_SYNC_FORCE=0`), and let meetscribe's existing schedule/team-gate
127
+ decide what to push.
128
+
129
+ ## Quick start (scribe client)
130
+
131
+ ```bash
132
+ # Install vezir + meetscribe-record (lightweight; ~30 MB).
133
+ pip install --user vezir
134
+
135
+ # Optional: GUI widget (Tkinter); on Debian/Ubuntu:
136
+ sudo apt install python3-tk
137
+
138
+ # Configure (one-time): server URL = Tailscale name of your vezir server
139
+ export VEZIR_URL=http://your-vezir-server:8000
140
+ export VEZIR_TOKEN=<token-issued-on-server>
141
+
142
+ # CLI scribe
143
+ vezir scribe --title "what this meeting is about"
144
+ # Talk; Ctrl+C when done.
145
+
146
+ # Or GUI scribe (always-on-top widget)
147
+ vezir gui
148
+ ```
149
+
150
+ When the recording is uploaded, vezir prints a dashboard URL. Open it in
151
+ your browser; the GUI's "Open dashboard" button does this for you. The
152
+ URL flows through `/login?token=...` so the browser is signed in via
153
+ HttpOnly cookie before it lands on the session page; subsequent access
154
+ from the same browser does not require re-passing the token.
155
+
156
+ ## Environment variables
157
+
158
+ | Variable | Default | Effect |
159
+ |---|---|---|
160
+ | `VEZIR_DATA` | `~/vezir-data` | All runtime state — sessions, voiceprints, queue, tokens, sync_config |
161
+ | `VEZIR_HOST` | `0.0.0.0` | Bind address for `vezir serve` |
162
+ | `VEZIR_PORT` | `8000` | Port for `vezir serve` |
163
+ | `VEZIR_URL` | `http://localhost:8000` | Server URL for `vezir scribe` clients |
164
+ | `VEZIR_TOKEN` | — | Bearer token for `vezir scribe` clients |
165
+ | `VEZIR_LOG_LEVEL` | `INFO` | Logging level |
166
+ | `VEZIR_MEET_BIN` | `$(which meet)` | Path to meetscribe `meet` binary |
167
+ | `VEZIR_SKIP_SYNC` | unset | Set to `1` to skip the `meet sync` step entirely |
168
+ | `VEZIR_DELETE_AUDIO` | unset | Set to `1` to delete audio after artifacts are produced (storage policy). Default OFF during pilot. |
169
+ | `VEZIR_SYNC_MEETING_TYPE` | `sandbox` | Subfolder name (under `meetings/`) used by `meet sync --force`. Will be removed once vezir respects schedules. |
170
+
171
+ ## License
172
+
173
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,74 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "vezir"
7
+ version = "0.1.0"
8
+ description = "Internal scribe service that wraps meetscribe for team-scale meeting capture, transcription, summarization, and speaker labeling"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = {text = "MIT"}
12
+ authors = [{name = "Blink"}]
13
+ keywords = [
14
+ "meeting",
15
+ "transcription",
16
+ "scribe",
17
+ "team",
18
+ "self-hosted",
19
+ ]
20
+ classifiers = [
21
+ "Development Status :: 3 - Alpha",
22
+ "Environment :: Console",
23
+ "Environment :: Web Environment",
24
+ "Intended Audience :: Developers",
25
+ "License :: OSI Approved :: MIT License",
26
+ "Operating System :: POSIX :: Linux",
27
+ "Operating System :: MacOS",
28
+ "Programming Language :: Python :: 3",
29
+ "Programming Language :: Python :: 3.10",
30
+ "Programming Language :: Python :: 3.11",
31
+ "Programming Language :: Python :: 3.12",
32
+ "Topic :: Multimedia :: Sound/Audio :: Speech",
33
+ "Topic :: Office/Business",
34
+ ]
35
+ dependencies = [
36
+ # Client-side base. Includes meetscribe-record for `vezir scribe`'s
37
+ # capture wrapper. Does NOT pull in transcription / diarization /
38
+ # summarization deps; those land via `pip install vezir[server]`.
39
+ "click>=8.0",
40
+ "httpx>=0.27",
41
+ "ulid-py>=1.1",
42
+ "meetscribe-record>=0.1.0",
43
+ ]
44
+
45
+ [project.scripts]
46
+ vezir = "vezir.cli:main"
47
+
48
+ [project.urls]
49
+ Homepage = "https://github.com/pretyflaco/vezir"
50
+ Repository = "https://github.com/pretyflaco/vezir"
51
+ Issues = "https://github.com/pretyflaco/vezir/issues"
52
+
53
+ [tool.setuptools.packages.find]
54
+ where = ["."]
55
+ include = ["vezir*"]
56
+
57
+ [tool.setuptools.package-data]
58
+ vezir = ["web/templates/*.html", "web/static/*"]
59
+
60
+ [project.optional-dependencies]
61
+ # Server side: full FastAPI app + worker that shells out to meetscribe-offline
62
+ # (transcribe, label, sync). Install on the GPU box that runs `vezir serve`.
63
+ server = [
64
+ "fastapi>=0.110",
65
+ "uvicorn[standard]>=0.27",
66
+ "python-multipart>=0.0.9",
67
+ "jinja2>=3.1",
68
+ "meetscribe-offline>=0.5.0",
69
+ ]
70
+ # GUI scribe widget (Tkinter). Tkinter ships with Python on most distros
71
+ # but some (Debian/Ubuntu minimal) require `apt install python3-tk`.
72
+ # No new pip deps needed.
73
+ gui = []
74
+ dev = ["ruff", "pytest", "pytest-asyncio"]
vezir-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,180 @@
1
+ """Tests for browser-friendly auth (cookie + bearer combined)."""
2
+ from __future__ import annotations
3
+
4
+ import io
5
+ import tempfile
6
+ import wave
7
+ from pathlib import Path
8
+
9
+ import pytest
10
+
11
+
12
+ @pytest.fixture
13
+ def tmp_data(monkeypatch):
14
+ with tempfile.TemporaryDirectory() as d:
15
+ monkeypatch.setenv("VEZIR_DATA", d)
16
+ yield Path(d)
17
+
18
+
19
+ @pytest.fixture
20
+ def client_and_token(tmp_data):
21
+ from fastapi.testclient import TestClient
22
+ from vezir.server import auth
23
+ from vezir.server.app import create_app
24
+
25
+ token = auth.issue("alice")
26
+ app = create_app()
27
+ return TestClient(app, follow_redirects=False), token
28
+
29
+
30
+ def _bearer(token: str) -> dict:
31
+ return {"Authorization": f"Bearer {token}"}
32
+
33
+
34
+ # ── /login GET with token (the GUI hand-off) ────────────────────────────────
35
+
36
+
37
+ def test_login_get_valid_token_sets_cookie_and_redirects(client_and_token):
38
+ client, token = client_and_token
39
+ resp = client.get(f"/login?token={token}&next=/s/abc123")
40
+ assert resp.status_code == 303
41
+ assert resp.headers["location"] == "/s/abc123"
42
+ sc = resp.headers.get("set-cookie", "")
43
+ assert "vezir_session=" in sc
44
+ assert "HttpOnly" in sc
45
+ assert "SameSite=lax" in sc.lower() or "samesite=lax" in sc.lower()
46
+
47
+
48
+ def test_login_get_invalid_token_returns_401_form(client_and_token):
49
+ client, _ = client_and_token
50
+ resp = client.get("/login?token=vzr_bogus&next=/s/abc")
51
+ assert resp.status_code == 401
52
+ assert "Invalid token" in resp.text
53
+ assert "vezir_session=" not in resp.headers.get("set-cookie", "")
54
+
55
+
56
+ def test_login_get_no_token_renders_form(client_and_token):
57
+ client, _ = client_and_token
58
+ resp = client.get("/login")
59
+ assert resp.status_code == 200
60
+ assert "Sign in" in resp.text
61
+ assert "<form" in resp.text
62
+
63
+
64
+ # ── /login open-redirect protection ─────────────────────────────────────────
65
+
66
+
67
+ @pytest.mark.parametrize("bad_next", [
68
+ "//evil.example.com",
69
+ "http://evil.example.com",
70
+ "https://evil.example.com/x",
71
+ "javascript:alert(1)",
72
+ "no-leading-slash",
73
+ ])
74
+ def test_login_rejects_unsafe_next(client_and_token, bad_next):
75
+ client, token = client_and_token
76
+ resp = client.get(f"/login?token={token}&next={bad_next}")
77
+ assert resp.status_code == 303
78
+ assert resp.headers["location"] == "/"
79
+
80
+
81
+ # ── POST /login (manual paste-token form) ───────────────────────────────────
82
+
83
+
84
+ def test_login_post_valid_token_sets_cookie(client_and_token):
85
+ client, token = client_and_token
86
+ resp = client.post("/login", data={"token": token, "next": "/"})
87
+ assert resp.status_code == 303
88
+ assert resp.headers["location"] == "/"
89
+ assert "vezir_session=" in resp.headers.get("set-cookie", "")
90
+
91
+
92
+ def test_login_post_invalid_token_401(client_and_token):
93
+ client, _ = client_and_token
94
+ resp = client.post("/login", data={"token": "vzr_bogus", "next": "/"})
95
+ assert resp.status_code == 401
96
+ assert "Invalid token" in resp.text
97
+
98
+
99
+ # ── /logout ─────────────────────────────────────────────────────────────────
100
+
101
+
102
+ def test_logout_clears_cookie(client_and_token):
103
+ client, token = client_and_token
104
+ # log in
105
+ r1 = client.get(f"/login?token={token}&next=/")
106
+ assert "vezir_session=" in r1.headers["set-cookie"]
107
+ # logout
108
+ r2 = client.get("/logout")
109
+ assert r2.status_code == 303
110
+ assert r2.headers["location"] == "/login"
111
+ sc = r2.headers.get("set-cookie", "")
112
+ # Cookie clearing has either Max-Age=0 or expired date in the past
113
+ assert "vezir_session=" in sc
114
+ assert ('Max-Age=0' in sc) or ('expires=' in sc.lower())
115
+
116
+
117
+ # ── Dashboard accepts cookie OR bearer ───────────────────────────────────────
118
+
119
+
120
+ def test_dashboard_with_cookie(client_and_token):
121
+ client, token = client_and_token
122
+ client.get(f"/login?token={token}&next=/") # establishes cookie
123
+ resp = client.get("/")
124
+ assert resp.status_code == 200
125
+ assert "Recent sessions" in resp.text or "No sessions yet" in resp.text
126
+
127
+
128
+ def test_dashboard_with_bearer(client_and_token):
129
+ client, token = client_and_token
130
+ resp = client.get("/", headers=_bearer(token))
131
+ assert resp.status_code == 200
132
+
133
+
134
+ def test_dashboard_no_auth_401(client_and_token):
135
+ client, _ = client_and_token
136
+ resp = client.get("/")
137
+ assert resp.status_code == 401
138
+
139
+
140
+ # ── API stays bearer-only (cookie should NOT grant API access) ──────────────
141
+
142
+
143
+ def test_api_sessions_rejects_cookie(client_and_token):
144
+ client, token = client_and_token
145
+ client.get(f"/login?token={token}&next=/") # set cookie
146
+ # Cookie-only — must fail without bearer header
147
+ resp = client.get("/api/sessions")
148
+ assert resp.status_code == 401
149
+
150
+
151
+ def test_api_sessions_accepts_bearer(client_and_token):
152
+ client, token = client_and_token
153
+ resp = client.get("/api/sessions", headers=_bearer(token))
154
+ assert resp.status_code == 200
155
+ assert "sessions" in resp.json()
156
+
157
+
158
+ # ── Upload endpoint produces dashboard_login_url ────────────────────────────
159
+
160
+
161
+ def test_upload_response_includes_login_url(client_and_token):
162
+ client, token = client_and_token
163
+ # tiny fake WAV
164
+ buf = io.BytesIO()
165
+ with wave.open(buf, "wb") as w:
166
+ w.setnchannels(1); w.setsampwidth(2); w.setframerate(16000)
167
+ w.writeframes(b"\x00\x00" * 16000)
168
+ buf.seek(0)
169
+
170
+ resp = client.post(
171
+ "/upload",
172
+ headers=_bearer(token),
173
+ files={"audio": ("foo.wav", buf.read(), "audio/wav")},
174
+ )
175
+ assert resp.status_code == 200
176
+ body = resp.json()
177
+ assert "dashboard_url" in body
178
+ assert "dashboard_login_url" in body
179
+ assert "/login?token=" in body["dashboard_login_url"]
180
+ assert f"%2Fs%2F{body['session_id']}" in body["dashboard_login_url"]