buzzkit 0.1.3__tar.gz → 0.1.4__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.
- {buzzkit-0.1.3 → buzzkit-0.1.4}/.github/workflows/CI.yml +6 -0
- {buzzkit-0.1.3 → buzzkit-0.1.4}/.github/workflows/release.yml +4 -0
- {buzzkit-0.1.3 → buzzkit-0.1.4}/Cargo.lock +1 -1
- {buzzkit-0.1.3 → buzzkit-0.1.4}/Cargo.toml +1 -1
- {buzzkit-0.1.3 → buzzkit-0.1.4}/PKG-INFO +2 -1
- buzzkit-0.1.4/examples/analyze_audio_tap.py +218 -0
- buzzkit-0.1.4/examples/huddle_wire_recorder.py +123 -0
- {buzzkit-0.1.3 → buzzkit-0.1.4}/pyproject.toml +2 -1
- {buzzkit-0.1.3 → buzzkit-0.1.4}/python/buzzkit/client.py +1 -3
- buzzkit-0.1.4/python/buzzkit/huddle.py +604 -0
- {buzzkit-0.1.3 → buzzkit-0.1.4}/src/huddle.rs +20 -8
- {buzzkit-0.1.3 → buzzkit-0.1.4}/tests/test_huddle.py +76 -16
- {buzzkit-0.1.3 → buzzkit-0.1.4}/tests/test_live_relay.py +3 -9
- buzzkit-0.1.3/python/buzzkit/huddle.py +0 -368
- {buzzkit-0.1.3 → buzzkit-0.1.4}/.cargo/config.toml +0 -0
- {buzzkit-0.1.3 → buzzkit-0.1.4}/.gitignore +0 -0
- {buzzkit-0.1.3 → buzzkit-0.1.4}/LICENSE +0 -0
- {buzzkit-0.1.3 → buzzkit-0.1.4}/LICENSE-APACHE +0 -0
- {buzzkit-0.1.3 → buzzkit-0.1.4}/NOTICE +0 -0
- {buzzkit-0.1.3 → buzzkit-0.1.4}/README.md +0 -0
- {buzzkit-0.1.3 → buzzkit-0.1.4}/build-support/vendored-libopus.cmake +0 -0
- {buzzkit-0.1.3 → buzzkit-0.1.4}/examples/_shared.py +0 -0
- {buzzkit-0.1.3 → buzzkit-0.1.4}/examples/claim_invite.py +0 -0
- {buzzkit-0.1.3 → buzzkit-0.1.4}/examples/huddle_echo.py +0 -0
- {buzzkit-0.1.3 → buzzkit-0.1.4}/examples/list_channels.py +0 -0
- {buzzkit-0.1.3 → buzzkit-0.1.4}/examples/offline_sign.py +0 -0
- {buzzkit-0.1.3 → buzzkit-0.1.4}/examples/send_message.py +0 -0
- {buzzkit-0.1.3 → buzzkit-0.1.4}/examples/set_profile.py +0 -0
- {buzzkit-0.1.3 → buzzkit-0.1.4}/examples/subscribe.py +0 -0
- {buzzkit-0.1.3 → buzzkit-0.1.4}/python/buzzkit/__init__.py +0 -0
- {buzzkit-0.1.3 → buzzkit-0.1.4}/python/buzzkit/_native.pyi +0 -0
- {buzzkit-0.1.3 → buzzkit-0.1.4}/python/buzzkit/py.typed +0 -0
- {buzzkit-0.1.3 → buzzkit-0.1.4}/src/lib.rs +0 -0
- {buzzkit-0.1.3 → buzzkit-0.1.4}/tests/test_signing.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: buzzkit
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Classifier: Development Status :: 3 - Alpha
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -16,6 +16,7 @@ Requires-Dist: websockets>=13
|
|
|
16
16
|
Requires-Dist: pytest>=8 ; extra == 'dev'
|
|
17
17
|
Requires-Dist: pytest-asyncio>=0.24 ; extra == 'dev'
|
|
18
18
|
Requires-Dist: ruff>=0.6 ; extra == 'dev'
|
|
19
|
+
Requires-Dist: ty>=0.0.60 ; extra == 'dev'
|
|
19
20
|
Requires-Dist: maturin>=1.7,<2 ; extra == 'dev'
|
|
20
21
|
Provides-Extra: dev
|
|
21
22
|
License-File: LICENSE
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""Analyze audio tap captures and point at the failing link.
|
|
2
|
+
|
|
3
|
+
Reads a capture directory produced by either (or both):
|
|
4
|
+
* roomkit's ``examples/buzz_voice_agent.py`` run with ``BUZZ_TAP_DIR=...``
|
|
5
|
+
(sender-side taps: provider_out_24k.raw, pacer_in_48k.raw,
|
|
6
|
+
wire_out_48k.raw, wire_send.csv, events.csv), or
|
|
7
|
+
* ``examples/huddle_wire_recorder.py`` (receiver-side: recv_frames.csv,
|
|
8
|
+
peer<N>_48k.raw).
|
|
9
|
+
|
|
10
|
+
and reports, per stage: send/arrival timing (stalls, bursts), silence frames
|
|
11
|
+
spliced into speech, wire sequence gaps, and audible holes inside the PCM.
|
|
12
|
+
|
|
13
|
+
python examples/analyze_audio_tap.py <capture_dir>
|
|
14
|
+
|
|
15
|
+
Reading the verdict:
|
|
16
|
+
* holes in wire_out_48k.raw but not in pacer_in_48k.raw
|
|
17
|
+
-> the pacer inserted silence / dropped audio (sender side).
|
|
18
|
+
* clean wire_out but recv_frames.csv shows seq gaps or heavy arrival jitter
|
|
19
|
+
-> frames lost or bunched between agent and relay fan-out.
|
|
20
|
+
* clean + smooth arrivals at the recorder, but the app still sounds choppy
|
|
21
|
+
-> receiver side (NetEq/playout in the desktop app).
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import csv
|
|
27
|
+
import pathlib
|
|
28
|
+
import sys
|
|
29
|
+
|
|
30
|
+
FRAME_MS = 20.0
|
|
31
|
+
NS = 1_000_000 # ns per ms
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def fmt_ts(ms: float) -> str:
|
|
35
|
+
return f"{int(ms // 60000):02d}:{ms % 60000 / 1000:06.3f}"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ---------------------------------------------------------------- timing CSVs
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def analyze_send_timing(rows: list[dict], t_key: str, label: str) -> None:
|
|
42
|
+
"""Inter-send/arrival timing: stalls (>2x frame) and bursts (<2 ms)."""
|
|
43
|
+
times = [int(r[t_key]) for r in rows]
|
|
44
|
+
if len(times) < 2:
|
|
45
|
+
print(f" {label}: not enough rows")
|
|
46
|
+
return
|
|
47
|
+
deltas = [(b - a) / NS for a, b in zip(times, times[1:])]
|
|
48
|
+
span_s = (times[-1] - times[0]) / NS / 1000
|
|
49
|
+
stalls = [(i, d) for i, d in enumerate(deltas) if d > 2 * FRAME_MS]
|
|
50
|
+
bursts = sum(1 for d in deltas if d < 2.0)
|
|
51
|
+
print(
|
|
52
|
+
f" {label}: {len(times)} frames over {span_s:.1f}s "
|
|
53
|
+
f"(mean {sum(deltas) / len(deltas):.1f} ms/frame, max gap {max(deltas):.0f} ms)"
|
|
54
|
+
)
|
|
55
|
+
print(f" gaps >{2 * FRAME_MS:.0f} ms: {len(stalls)} sends <2 ms apart (bursts): {bursts}")
|
|
56
|
+
t0 = times[0]
|
|
57
|
+
for i, d in stalls[:10]:
|
|
58
|
+
print(f" gap {d:6.0f} ms at t+{fmt_ts((times[i] - t0) / NS)}")
|
|
59
|
+
if len(stalls) > 10:
|
|
60
|
+
print(f" … and {len(stalls) - 10} more")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def analyze_wire_csv(path: pathlib.Path) -> None:
|
|
64
|
+
rows = list(csv.DictReader(path.open()))
|
|
65
|
+
if not rows:
|
|
66
|
+
print(" wire_send.csv: empty")
|
|
67
|
+
return
|
|
68
|
+
print("wire_send.csv — pacer output timing (what buzzkit is asked to send):")
|
|
69
|
+
analyze_send_timing(rows, "t_mono_ns", "pacer->send_pcm")
|
|
70
|
+
|
|
71
|
+
# Silence frames spliced into speech: an all-zero send whose neighbours
|
|
72
|
+
# within 300 ms on both sides contain non-zero audio.
|
|
73
|
+
times = [int(r["t_mono_ns"]) for r in rows]
|
|
74
|
+
zero = [r["all_zero"] == "1" for r in rows]
|
|
75
|
+
t0 = times[0]
|
|
76
|
+
nonzero_times = [t for t, z in zip(times, zero) if not z]
|
|
77
|
+
spliced = []
|
|
78
|
+
for t, z in zip(times, zero):
|
|
79
|
+
if not z or not nonzero_times:
|
|
80
|
+
continue
|
|
81
|
+
# nearest real-audio sends before/after this silence frame
|
|
82
|
+
before = any(0 < t - u <= 300 * NS for u in nonzero_times)
|
|
83
|
+
after = any(0 < u - t <= 300 * NS for u in nonzero_times)
|
|
84
|
+
if before and after:
|
|
85
|
+
spliced.append(t)
|
|
86
|
+
n_zero = sum(zero)
|
|
87
|
+
print(f" all-zero frames: {n_zero} total, {len(spliced)} spliced mid-speech")
|
|
88
|
+
for t in spliced[:10]:
|
|
89
|
+
print(f" silence splice at t+{fmt_ts((t - t0) / NS)}")
|
|
90
|
+
if len(spliced) > 10:
|
|
91
|
+
print(f" … and {len(spliced) - 10} more")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def analyze_recv_csv(path: pathlib.Path) -> None:
|
|
95
|
+
rows = list(csv.DictReader(path.open()))
|
|
96
|
+
if not rows:
|
|
97
|
+
print(" recv_frames.csv: empty")
|
|
98
|
+
return
|
|
99
|
+
print("recv_frames.csv — wire arrivals at the recorder (per peer):")
|
|
100
|
+
peers: dict[str, list[dict]] = {}
|
|
101
|
+
for r in rows:
|
|
102
|
+
peers.setdefault(r["peer_index"], []).append(r)
|
|
103
|
+
for peer, frames in sorted(peers.items()):
|
|
104
|
+
speech = [f for f in frames if f["is_dtx"] == "0"]
|
|
105
|
+
dtx = len(frames) - len(speech)
|
|
106
|
+
print(f" peer {peer}: {len(speech)} speech frames, {dtx} DTX")
|
|
107
|
+
if len(speech) < 2:
|
|
108
|
+
continue
|
|
109
|
+
analyze_send_timing(speech, "t_mono_ns", f"peer {peer} arrivals")
|
|
110
|
+
# Wire continuity over ALL frames (DTX included — every wire packet
|
|
111
|
+
# consumes one seq and 960 ts): dseq != 1 means frames were lost or
|
|
112
|
+
# reordered between the sender and us; dts != dseq*960 means the
|
|
113
|
+
# sender's own media timeline jumped (encoder restart / bug).
|
|
114
|
+
lost = jumps = 0
|
|
115
|
+
for a, b in zip(frames, frames[1:]):
|
|
116
|
+
dseq = (int(b["seq"]) - int(a["seq"])) % 65536
|
|
117
|
+
dts = (int(b["ts_48k"]) - int(a["ts_48k"])) % (1 << 32)
|
|
118
|
+
if dseq != 1:
|
|
119
|
+
lost += 1
|
|
120
|
+
if lost <= 10:
|
|
121
|
+
print(f" lost/reordered: seq {a['seq']}->{b['seq']} ({dseq - 1} missing)")
|
|
122
|
+
elif dts != 960:
|
|
123
|
+
jumps += 1
|
|
124
|
+
if jumps <= 10:
|
|
125
|
+
print(f" sender ts jump at seq {b['seq']}: +{dts} (expected +960)")
|
|
126
|
+
print(f" lost/reordered frames: {lost} sender ts jumps: {jumps}")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# ------------------------------------------------------------------ PCM scans
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def analyze_raw(path: pathlib.Path, rate: int) -> None:
|
|
133
|
+
"""Find near-silent runs >=15 ms that sit inside speech (audible holes)."""
|
|
134
|
+
data = path.read_bytes()
|
|
135
|
+
samples = memoryview(data).cast("h")
|
|
136
|
+
n = len(samples)
|
|
137
|
+
if n == 0:
|
|
138
|
+
print(f" {path.name}: empty")
|
|
139
|
+
return
|
|
140
|
+
ms = 1000.0 / rate
|
|
141
|
+
win = rate // 100 # 10 ms windows
|
|
142
|
+
# Classify 10 ms windows as silent/speech, then find silent runs
|
|
143
|
+
# bordered by speech within 200 ms on each side.
|
|
144
|
+
n_win = n // win
|
|
145
|
+
loud = []
|
|
146
|
+
for w in range(n_win):
|
|
147
|
+
seg = samples[w * win : (w + 1) * win]
|
|
148
|
+
peak = max(abs(min(seg)), abs(max(seg)))
|
|
149
|
+
loud.append(peak > 400)
|
|
150
|
+
holes: list[tuple[int, int]] = []
|
|
151
|
+
w = 0
|
|
152
|
+
while w < n_win:
|
|
153
|
+
if loud[w]:
|
|
154
|
+
w += 1
|
|
155
|
+
continue
|
|
156
|
+
start = w
|
|
157
|
+
while w < n_win and not loud[w]:
|
|
158
|
+
w += 1
|
|
159
|
+
run_ms = (w - start) * 10
|
|
160
|
+
ctx = 20 # 200 ms of context windows
|
|
161
|
+
speech_before = any(loud[max(0, start - ctx) : start])
|
|
162
|
+
speech_after = any(loud[w : w + ctx])
|
|
163
|
+
if run_ms >= 15 and run_ms <= 2000 and speech_before and speech_after:
|
|
164
|
+
holes.append((start, run_ms))
|
|
165
|
+
dur_s = n * ms / 1000
|
|
166
|
+
print(f" {path.name}: {dur_s:.1f}s of audio, {len(holes)} hole(s) inside speech")
|
|
167
|
+
for start, run_ms in holes[:15]:
|
|
168
|
+
print(f" {run_ms:4d} ms hole at {fmt_ts(start * 10.0)}")
|
|
169
|
+
if len(holes) > 15:
|
|
170
|
+
print(f" … and {len(holes) - 15} more")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
RAW_RATES = {"24k": 24_000, "48k": 48_000, "16k": 16_000}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def main() -> None:
|
|
177
|
+
if len(sys.argv) != 2:
|
|
178
|
+
raise SystemExit("usage: analyze_audio_tap.py <capture_dir>")
|
|
179
|
+
cap = pathlib.Path(sys.argv[1])
|
|
180
|
+
if not cap.is_dir():
|
|
181
|
+
raise SystemExit(f"not a directory: {cap}")
|
|
182
|
+
|
|
183
|
+
found = False
|
|
184
|
+
wire_csv = cap / "wire_send.csv"
|
|
185
|
+
if wire_csv.exists():
|
|
186
|
+
found = True
|
|
187
|
+
analyze_wire_csv(wire_csv)
|
|
188
|
+
print()
|
|
189
|
+
recv_csv = cap / "recv_frames.csv"
|
|
190
|
+
if recv_csv.exists():
|
|
191
|
+
found = True
|
|
192
|
+
analyze_recv_csv(recv_csv)
|
|
193
|
+
print()
|
|
194
|
+
events_csv = cap / "events.csv"
|
|
195
|
+
if events_csv.exists():
|
|
196
|
+
rows = list(csv.DictReader(events_csv.open()))
|
|
197
|
+
interesting = [r for r in rows if r.get("event") not in (None, "")]
|
|
198
|
+
if interesting:
|
|
199
|
+
print(f"events.csv: {len(interesting)} event(s)")
|
|
200
|
+
for r in interesting[:20]:
|
|
201
|
+
print(f" {r}")
|
|
202
|
+
print()
|
|
203
|
+
raws = sorted(cap.glob("*.raw"))
|
|
204
|
+
if raws:
|
|
205
|
+
found = True
|
|
206
|
+
print("PCM hole scan (near-silence >=15 ms surrounded by speech):")
|
|
207
|
+
for raw in raws:
|
|
208
|
+
rate = next(
|
|
209
|
+
(hz for tag, hz in RAW_RATES.items() if tag in raw.name),
|
|
210
|
+
48_000,
|
|
211
|
+
)
|
|
212
|
+
analyze_raw(raw, rate)
|
|
213
|
+
if not found:
|
|
214
|
+
raise SystemExit(f"no tap files found in {cap}")
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
if __name__ == "__main__":
|
|
218
|
+
main()
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Wire-side recorder for Buzz huddles — capture exactly what receivers get.
|
|
2
|
+
|
|
3
|
+
Joins a huddle as a silent peer (sends nothing) and records every inbound
|
|
4
|
+
audio frame with its arrival time and wire header. This is the ground truth
|
|
5
|
+
for "what does the desktop app's jitter buffer actually see": if the audio
|
|
6
|
+
is already choppy here, the sender (e.g. the roomkit agent) is at fault; if
|
|
7
|
+
it is clean here but choppy in the app, the receiver side is at fault.
|
|
8
|
+
|
|
9
|
+
Watch mode (recommended): give it only the parent channel — the one in your
|
|
10
|
+
BUZZ_CHANNEL_ID — and it records every huddle announced there (kind 48100),
|
|
11
|
+
one output directory per huddle:
|
|
12
|
+
|
|
13
|
+
BUZZ_RELAY_URL=wss://... python examples/huddle_wire_recorder.py \
|
|
14
|
+
<parent_channel_id>
|
|
15
|
+
|
|
16
|
+
Direct mode, when you already know the ephemeral huddle id (the agent logs
|
|
17
|
+
it as "Joined huddle <uuid>"):
|
|
18
|
+
|
|
19
|
+
python examples/huddle_wire_recorder.py <parent_channel_id> \
|
|
20
|
+
<huddle_channel_id> [out_dir]
|
|
21
|
+
|
|
22
|
+
Output (out_dir, default ./wire-tap-<huddle>):
|
|
23
|
+
recv_frames.csv one row per wire frame:
|
|
24
|
+
t_mono_ns,peer_index,seq,ts_48k,level_dbov,is_dtx,pcm_bytes
|
|
25
|
+
events.csv roster events: t_mono_ns,event,pubkey
|
|
26
|
+
peer<N>_48k.raw decoded s16le mono 48 kHz PCM per peer (non-DTX frames,
|
|
27
|
+
concatenated in arrival order — listen with:
|
|
28
|
+
ffplay -f s16le -ar 48000 -ch_layout mono peer<N>_48k.raw)
|
|
29
|
+
|
|
30
|
+
Analyze the capture with examples/analyze_audio_tap.py.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import asyncio
|
|
36
|
+
import json
|
|
37
|
+
import pathlib
|
|
38
|
+
import sys
|
|
39
|
+
import time
|
|
40
|
+
|
|
41
|
+
from _shared import agent_secret, relay_url
|
|
42
|
+
from buzzkit import KIND_HUDDLE_STARTED, BuzzClient, HuddleAudio, HuddleClient
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def record(
|
|
46
|
+
relay: str, secret: str, huddle_id: str, parent_id: str, out: pathlib.Path
|
|
47
|
+
) -> None:
|
|
48
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
49
|
+
frames_csv = (out / "recv_frames.csv").open("w")
|
|
50
|
+
frames_csv.write("t_mono_ns,peer_index,seq,ts_48k,level_dbov,is_dtx,pcm_bytes\n")
|
|
51
|
+
events_csv = (out / "events.csv").open("w")
|
|
52
|
+
events_csv.write("t_mono_ns,event,pubkey\n")
|
|
53
|
+
pcm_files: dict[int, object] = {}
|
|
54
|
+
n_frames = 0
|
|
55
|
+
|
|
56
|
+
# paced=False: the client never sends anything on its own (no silence
|
|
57
|
+
# stream), so this peer is acoustically invisible in the huddle.
|
|
58
|
+
client = HuddleClient(relay, secret, huddle_id, parent_channel_id=parent_id, paced=False)
|
|
59
|
+
async with client:
|
|
60
|
+
print(f"recording huddle {huddle_id} -> {out} (Ctrl+C to stop)")
|
|
61
|
+
try:
|
|
62
|
+
async for ev in client.events():
|
|
63
|
+
now = time.monotonic_ns()
|
|
64
|
+
if isinstance(ev, HuddleAudio):
|
|
65
|
+
frames_csv.write(
|
|
66
|
+
f"{now},{ev.peer_index},{ev.seq},{ev.ts_48k},"
|
|
67
|
+
f"{ev.level_dbov},{int(ev.is_dtx)},{len(ev.pcm)}\n"
|
|
68
|
+
)
|
|
69
|
+
if not ev.is_dtx:
|
|
70
|
+
f = pcm_files.get(ev.peer_index)
|
|
71
|
+
if f is None:
|
|
72
|
+
f = (out / f"peer{ev.peer_index}_48k.raw").open("wb")
|
|
73
|
+
pcm_files[ev.peer_index] = f
|
|
74
|
+
f.write(ev.pcm)
|
|
75
|
+
n_frames += 1
|
|
76
|
+
if n_frames % 500 == 0:
|
|
77
|
+
frames_csv.flush()
|
|
78
|
+
print(f" {n_frames} frames…")
|
|
79
|
+
else:
|
|
80
|
+
events_csv.write(f"{now},{type(ev).__name__},{ev.pubkey}\n")
|
|
81
|
+
events_csv.flush()
|
|
82
|
+
finally:
|
|
83
|
+
frames_csv.close()
|
|
84
|
+
events_csv.close()
|
|
85
|
+
for f in pcm_files.values():
|
|
86
|
+
f.close()
|
|
87
|
+
print(f"done: {n_frames} frames from {len(pcm_files)} speaking peer(s)")
|
|
88
|
+
print(f"analyze with: python examples/analyze_audio_tap.py {out}")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
async def watch_and_record(relay: str, secret: str, parent_id: str) -> None:
|
|
92
|
+
"""Record every huddle announced on the parent channel from now on."""
|
|
93
|
+
print(f"watching {parent_id} for huddles (kind {KIND_HUDDLE_STARTED})…")
|
|
94
|
+
async with BuzzClient(relay, secret) as bz:
|
|
95
|
+
live = {"kinds": [KIND_HUDDLE_STARTED], "#h": [parent_id], "since": int(time.time())}
|
|
96
|
+
async for event in bz.subscribe([live]):
|
|
97
|
+
huddle_id = json.loads(event["content"]).get("ephemeral_channel_id")
|
|
98
|
+
if not huddle_id:
|
|
99
|
+
continue
|
|
100
|
+
out = pathlib.Path(f"wire-tap-{huddle_id[:8]}")
|
|
101
|
+
await record(relay, secret, huddle_id, parent_id, out)
|
|
102
|
+
print(f"watching {parent_id} for the next huddle…")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
async def main() -> None:
|
|
106
|
+
if len(sys.argv) < 2:
|
|
107
|
+
raise SystemExit(
|
|
108
|
+
"usage: huddle_wire_recorder.py <parent_channel_id> [huddle_channel_id] [out_dir]"
|
|
109
|
+
)
|
|
110
|
+
parent_id = sys.argv[1]
|
|
111
|
+
if len(sys.argv) < 3:
|
|
112
|
+
await watch_and_record(relay_url(), agent_secret(), parent_id)
|
|
113
|
+
return
|
|
114
|
+
huddle_id = sys.argv[2]
|
|
115
|
+
out = pathlib.Path(sys.argv[3] if len(sys.argv) > 3 else f"wire-tap-{huddle_id[:8]}")
|
|
116
|
+
await record(relay_url(), agent_secret(), huddle_id, parent_id, out)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
if __name__ == "__main__":
|
|
120
|
+
try:
|
|
121
|
+
asyncio.run(main())
|
|
122
|
+
except KeyboardInterrupt:
|
|
123
|
+
pass
|
|
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "buzzkit"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.4"
|
|
8
8
|
description = "Python bindings + async client for Block's Buzz (Nostr) protocol, backed by Rust buzz-core/buzz-sdk"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -34,6 +34,7 @@ dev = [
|
|
|
34
34
|
"pytest>=8",
|
|
35
35
|
"pytest-asyncio>=0.24",
|
|
36
36
|
"ruff>=0.6",
|
|
37
|
+
"ty>=0.0.60",
|
|
37
38
|
"maturin>=1.7,<2",
|
|
38
39
|
]
|
|
39
40
|
|
|
@@ -153,9 +153,7 @@ class BuzzClient:
|
|
|
153
153
|
result = await self.publish(create_ev)
|
|
154
154
|
if not result["accepted"]:
|
|
155
155
|
raise RuntimeError(f"huddle channel rejected: {result['message']}")
|
|
156
|
-
started_ev = _native.build_huddle_started_event(
|
|
157
|
-
self._secret, parent_channel_id, huddle_id
|
|
158
|
-
)
|
|
156
|
+
started_ev = _native.build_huddle_started_event(self._secret, parent_channel_id, huddle_id)
|
|
159
157
|
result = await self.publish(started_ev)
|
|
160
158
|
if not result["accepted"]:
|
|
161
159
|
raise RuntimeError(f"huddle announcement rejected: {result['message']}")
|