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.
- vezir-0.1.0/LICENSE +21 -0
- vezir-0.1.0/PKG-INFO +216 -0
- vezir-0.1.0/README.md +173 -0
- vezir-0.1.0/pyproject.toml +74 -0
- vezir-0.1.0/setup.cfg +4 -0
- vezir-0.1.0/tests/test_auth.py +180 -0
- vezir-0.1.0/tests/test_queue.py +48 -0
- vezir-0.1.0/vezir/__init__.py +3 -0
- vezir-0.1.0/vezir/cli.py +184 -0
- vezir-0.1.0/vezir/client/__init__.py +0 -0
- vezir-0.1.0/vezir/client/gui.py +562 -0
- vezir-0.1.0/vezir/client/scribe.py +130 -0
- vezir-0.1.0/vezir/client/uploader.py +65 -0
- vezir-0.1.0/vezir/config.py +93 -0
- vezir-0.1.0/vezir/server/__init__.py +0 -0
- vezir-0.1.0/vezir/server/app.py +64 -0
- vezir-0.1.0/vezir/server/auth.py +163 -0
- vezir-0.1.0/vezir/server/labels.py +216 -0
- vezir-0.1.0/vezir/server/login.py +119 -0
- vezir-0.1.0/vezir/server/meet_runner.py +278 -0
- vezir-0.1.0/vezir/server/queue.py +153 -0
- vezir-0.1.0/vezir/server/sessions.py +97 -0
- vezir-0.1.0/vezir/server/templating.py +9 -0
- vezir-0.1.0/vezir/server/uploads.py +105 -0
- vezir-0.1.0/vezir/server/voiceprints.py +59 -0
- vezir-0.1.0/vezir/server/worker.py +363 -0
- vezir-0.1.0/vezir/web/__init__.py +0 -0
- vezir-0.1.0/vezir/web/static/style.css +175 -0
- vezir-0.1.0/vezir/web/templates/base.html +20 -0
- vezir-0.1.0/vezir/web/templates/dashboard.html +39 -0
- vezir-0.1.0/vezir/web/templates/label.html +56 -0
- vezir-0.1.0/vezir/web/templates/label_pending.html +10 -0
- vezir-0.1.0/vezir/web/templates/login.html +28 -0
- vezir-0.1.0/vezir/web/templates/session.html +32 -0
- vezir-0.1.0/vezir.egg-info/PKG-INFO +216 -0
- vezir-0.1.0/vezir.egg-info/SOURCES.txt +38 -0
- vezir-0.1.0/vezir.egg-info/dependency_links.txt +1 -0
- vezir-0.1.0/vezir.egg-info/entry_points.txt +2 -0
- vezir-0.1.0/vezir.egg-info/requires.txt +18 -0
- 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,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"]
|