web: voice notes — the community scribe posts the transcript as the message - #7348
Open
loviswaternakamoto wants to merge 5 commits into
Open
web: voice notes — the community scribe posts the transcript as the message#7348loviswaternakamoto wants to merge 5 commits into
loviswaternakamoto wants to merge 5 commits into
Conversation
Mirror of block/buzz, pinned at upstream commit 02f640b. License: Apache-2.0 (LICENSE carried; upstream has no NOTICE, so Apache §4(d) is N/A).
…a phone and the relay address lands in the room - web /join — paste the relay address; the community's pairing material (join.json, origin-relative, operator-gated: absent = plain refusal, never a guess) resolves the standing invite; a PERSISTENT browser-local key claims it (NIP-07 preferred whenever present, per the existing durable-membership gating); a phone-first room view opens on the live wire (NIP-42 AUTH → NIP-29 shape: kind 9, #h = channel id), history + composer, relay verdicts rendered verbatim, fail-closed throughout - canonical-origin signing law: NIP-98 u tags and NIP-42 relay tags must name the community's CANONICAL origin as the relay declares it (NIP-11 /info push.origin), while transport rides the road the user was given — proven against an alias-host deployment (relay.skaists.dev behind skaists.buzz) where signing the alias is refused 401 URL-mismatch - signer: optional persistent secretKey path (shared/lib/local-identity — generated once, stored locally, exportable as nsec, honestly labeled), so durable membership no longer REQUIRES an extension on phones that have none; requireNip07 flows unchanged - checks: pnpm check (biome + file-size + pubkey-truncation) green, tsc clean, vite build clean - proven LIVE on the estate hive (skaists.buzz / relay.skaists.dev, buzz 0.2.1 stock server): cold 390px phone context, no extension, address typed by hand → key made on the phone → standing invite claimed (role member) → NIP-42 ok on the alias road → room live, history rendered, message sent AND received; a relay with no published material refused in plain words
…dings
RELAY GUARD (defense in depth): kind 9 (channel message) content carrying a bech32 SECRET KEY is refused before ingest — content_leaks_secret matches the token shape (nsec1 + >=15 bech32 digits, a real nsec is ~63) so ordinary words containing nsec1 inside them pass and only a real key shape trips it; plain-words refusal: "invalid: that looks like a private key (nsec1...) -- never send a secret into a room"; 3 unit tests (real token refused, normal + substring-trap + short-token + other-kind pass). FORK-TO-PROVEN LIVE on the throwaway stack (rotate-test.local, our debug binary, fresh owner+member): mint 200 -> claim 200 joined -> NIP-42 AUTH ok -> nsec post OK FALSE with the guard refusal -> GUARD_VERDICT=PROVEN.
CLIENT: the composer refuses the same token shape BEFORE signing, plain words, draft kept with a clear-the-draft action (a broadcast cannot be unsent); the substring trap sends normally (precision proven live on the estate hive); the copy-the-secret control moved OFF the composer into its own bottom sheet opened from the header (key), carrying the never-paste warning beside the copy button — the control that handles the secret is never next to the control that broadcasts.
ROOM SWITCHER: join.json gains optional rooms [{id,name}] (operator-curated; malformed rooms refuse the whole material — fail-closed); the phone view renders the rooms as chips under the header, switching resets the pane and re-opens the live socket on the chosen channel; the join lands on default_channel. Rooms proven live: welcome-everyone · general · huddle · PLUR, a message read in two rooms.
checks: cargo test -p buzz-relay --lib secret_guard 2/2, cargo build -p buzz-relay green (a [cfg(test)] attribute placement bug in the first cut was caught by cargo build vs cargo test divergence — the helper must sit OUTSIDE the tests mod); pnpm check + tsc + vite build green.
…e message
A phone records a voice note in the room composer; the community's own
speech-to-text door (declared in join.json, fail-closed when absent)
transcribes it server-side (whisper.cpp, language PINNED per request,
never auto-detected) and the TRANSCRIPT is posted as the message,
carrying the audio's sha256. The raw audio is deleted by the door
after transcription — the room keeps text plus a digest, never a
recording.
- join-material: optional voice { path, tongues } — same fail-closed law
as rooms (a malformed declaration refuses the material, never a guess)
- room composer: mic (record, tap again to send), tongue picker in the
community's order (lv, th, ru, uk for skaists), honest transcribing
state, plain-language refusals, the nsec guard applied to transcripts
- NIP-98 auth for the door: u tag signs the CANONICAL origin, payload
tag binds the audio bytes, transport rides the given road — the invite
claim's law, applied
🔐 Codex Security Review
|
loviswaternakamoto
added a commit
to beehive-nature/beehive-nature
that referenced
this pull request
Sep 4, 2026
… down (voice lane Goal 1, fork-to-prove): a voice note in the phone room composer goes to the community's own speech-to-text door (relay.skaists.dev/voice + skaists.buzz/voice, both roads) and the TRANSCRIPT is posted as the message carrying the audio's sha256 — the raw audio is DELETED after transcription (spool dir per job, removed in a finally; the receipt run left it EMPTY, journal-line proved), so the room keeps text plus a digest, never a recording. THE MODEL BAKE-OFF decided the door: whisper-small garbles natural Latvian, whisper-medium is accurate but 4-6x real-time, LANDED = large-v3-turbo q5_0 (574MB) at ~2.6xRT with language PINNED -l lv (never auto-detect); tongue order lv · th · ru · uk enforced as the door's allowlist AND the picker's order, lv default everywhere. THE DOOR (ops/voice-scribe, in-tree verbatim = what runs on the box): node worker, NIP-98-gated (u tag must sign the canonical origin — with OR without the declared path's trailing slash, the law found live when a correct signature 401'd on the slash; payload tag = sha256 of the raw audio bytes; nonce single-use; 10 jobs/5min/key; serialized whisper queue; 8MB/120s caps; systemd voice-scribe.service on 172.18.0.1:8093; iptables INPUT allowlist rule + netfilter-persistent — without it caddy 502s 'Host is unreachable'); MediaRecorder webm carries NO container duration (no Cues) so the duration cap probes the CONVERTED wav, found live on receipt run two. THE CLIENT: fork skaists/buzz branch voice-messages @0384a753 (stacked on join-by-address), PR upstream block/buzz#7348 — mic rendered ONLY when join.json declares voice (fail-closed both ways: absent = no mic, malformed = whole material refused, the rooms law), tongue sheet in the community's order, honest transcribing state, plain-word refusals, the nsec guard applied to transcripts, recording abandoned on room switch, client stops at the door's 120s cap. Gates: biome+tsc+file-sizes+pubkey-truncation green (repo-wide biome's two remaining errors are the agent-roster seat's untracked WIP, untouched per the worktree law). THE RECEIPT: e2e/voice-shot.mjs 11/11 PASS — COLD phone at 390px joins by address ALONE, switches to #general, records 15.5s of REAL Latvian (LibriVox Lacplesis reader, public domain, fed through Chrome's fake mic capture — the real mic path, opus-encoded like production) and #general receives 'Sis ir LibriVox ieraksts. Visi LibriVox ierasti ir brivi no autortiesibam...' + the digest line — shot e2e/shots-buzz/voice-390-general-transcript.png; door-served digest matches the posted message byte-for-byte. FLAGS: turbo still drifts on archaic words (ieraksti->ierasti — honest transcript, not cleaned); 40.5s door time for a 15.5s note is the price of the accurate model; nonce/rate state is in-memory = single instance (redis is the multi-instance cure); membership stays the relay's law — the door proves key possession only; espeak-ng lv is UNRECOGNIZED by whisper (synthetic-voice testing needs neural TTS or real speech) and the msedge-tts free endpoint is dead 2026-09-04 (both banked). NEXT: th · ru · uk proof runs (allowlist already live), desktop-client voice, whisper batching if traffic grows. Co-authored-by: zCode <zcode@skaists.dev>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacks on #7311 (
join-by-address) — the room view it extends. Only the last commit is new.What
Voice notes for the phone room. Tap the mic, speak, tap again: the note goes to the community's own speech-to-text door, and the transcript is posted as the message, carrying the audio's sha256. The raw audio is deleted by the door after transcription — the room keeps text plus a digest, never a stored recording.
Proven live (the screenshot in the discussion): a cold phone joins by address alone, switches to
#general, records ~15s of real Latvian speech (LibriVox, public domain), and the message arrives asHow
join-material.ts— optionalvoice: { path, tongues? }injoin.json. Fail-closed both ways: absent → no mic is rendered (the capability is the operator's to declare, never the client's to assume); malformed → the whole material is refused, same law asrooms.voice.ts— recorder (MediaRecorder, mime cascade webm/opus → mp4 → ogg) + the door POST: NIP-98 kind 27235,utag signing the community's canonical origin while transport rides the road the stranger was given (the invite claim's law, applied),payloadtag binding the sha256 of the raw audio bytes.RoomView.tsx— mic in the composer (record → tap to send), tongue picker in the community's order, honest "transcribing…" state, plain-language refusals surfaced verbatim, the existing nsec guard applied to transcripts (defense in depth), recording abandoned on room switch, client-side stop at the door's 120s cap.The server half is one file of community ops (node → ffmpeg →
whisper-cli -l <tongue>, serialized, spool dirrm -rf'd in afinally) — relay untouched, so the client capability is the whole upstreamable surface. Language is pinned per request, never auto-detected; the reference deployment's tongue order is lv · th · ru · uk.