From 97a26ec4e64a98a24dad7969337d2569339db9a7 Mon Sep 17 00:00:00 2001 From: salmonik Date: Wed, 4 Nov 2026 01:33:33 +0100 Subject: docs: sync with current behavior --- .env.example | 6 ++++++ AGENTS.md | 49 +++++++++++++++++++++++++++---------------------- CONTEXT.md | 13 +++++++------ README.md | 21 +++++++++++++-------- src/config.rs | 8 ++++---- 5 files changed, 57 insertions(+), 40 deletions(-) diff --git a/.env.example b/.env.example index e67827d..83d3902 100644 --- a/.env.example +++ b/.env.example @@ -22,8 +22,14 @@ LLM_MODEL= OPENROUTER_API_KEY= OPENROUTER_MODEL= +# Optional - Voice recording (opt-in; no audio is kept when false) +RECORDING_ENABLED=false +# Rolling buffer length in seconds (1-3600). +RECORDING_WINDOW_SECONDS=900 + # Optional - Local TTS # Directory containing .onnx, or a full path to the .onnx file. +# There is no external TTS fallback; leave unset to disable TTS. TTS_MODEL_PATH=/path/to/piper TTS_VOICE=pl_PL-bass-high # Parent directory of espeak-ng-data. Docker overrides this to /usr/share. diff --git a/AGENTS.md b/AGENTS.md index 0c1c2d7..760e790 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,28 +4,33 @@ ``` src/ -├── main.rs — Entry: init tracing, dotenvy, build Config/Data, start poise+serenity -├── config.rs — Config struct (env-driven), pub const TEMP_PATH +├── main.rs — Entry: tracing, dotenvy, temp dir, Config/Data, poise+serenity +├── config.rs — Config struct (env-driven), temp dir init/access ├── error.rs — Unified Error enum + From impls, pub type Result +├── net.rs — public-IP check for user-supplied URLs (SSRF guard) ├── music.rs — MusicState: per-guild queue, now/next/last, skip, auto-advance +├── recording/mod.rs — per-guild rolling PCM recorder for /record +├── tts.rs — TtsState (piper-rs in-process, bounded DashMap cache, resampler, WAV encoder) ├── commands/ │ ├── mod.rs — all_commands() aggregates every submodule -│ ├── ai.rs — /senator (OpenRouter chat + optional TTS) +│ ├── ai.rs — /senator (LLM chat + optional TTS) +│ ├── autocomplete.rs — Copyparty autocomplete + trigger_name │ ├── cron.rs — /cron group (6 subcommands) -│ ├── math.rs — /math (qalc subprocess) +│ ├── leaderboard.rs — /leaderboard group (me/messages/active/afk/total) +│ ├── math.rs — /math (qalc subprocess, 5s timeout) │ ├── music.rs — /music group (grajcuj, skip, queue, stop, join) + context menu -│ ├── qr.rs — /qr encode + decode +│ ├── qr.rs — /qr encode + decode (dimension/allocation limits) +│ ├── record.rs — /record (flush this guild's buffer to Copyparty) │ ├── trigger.rs — /trigger group (6 subcommands) -│ ├── tts.rs — /tts (local Piper TTS → file/vc), /log_volume -│ ├── utilities.rs — compress🟡, convert🟡, upload, search, download, say +│ ├── tts.rs — /tts (local Piper TTS → file/vc) +│ ├── utilities.rs — compress, convert, upload, search, download, say │ └── web.rs — /web (DuckDuckGo scrape) -├── copyparty/mod.rs — CopypartyClient (search, upload) -├── database/mod.rs — DbPool, 21 query fns, 6 tables, migration -├── events/mod.rs — handle_ready, handle_voice_state_update, handle_message -├── openrouter/mod.rs — OpenRouterClient (chat, chat_with_history) -├── tasks/mod.rs — cron_scheduler_loop (30s polling, stub exec bodies) -├── tts.rs — TtsState (piper-rs in-process, DashMap cache, pre-gen, resampler, WAV encoder) -└── voice/mod.rs — VoiceManager (connect, disconnect, get_call), mp3_to_input +├── copyparty/mod.rs — CopypartyClient (search, upload, credential-safe URLs) +├── database/mod.rs — DbPool, migrations, query helpers (7 tables) +├── events/mod.rs — handle_ready, voice-state, message (triggers + LLM) +├── openrouter/mod.rs — OpenRouterClient (chat, chat_with_history, timeouts) +├── tasks/mod.rs — cron_scheduler_loop (60s polling, real execution) +└── voice/mod.rs — VoiceManager (connect, disconnect, get_call), ffmpeg/symphonia inputs ``` ## Key Conventions @@ -35,18 +40,18 @@ src/ - **Errors**: `crate::error::Error` enum with `?` propagation; use `Error::Custom(string)` for ad-hoc - **Shared state**: `Data` struct in `main.rs` — passed as poise `user_data` - **Commands**: each submodule exports `pub fn commands() -> Vec>` -- **Temp path**: always use `crate::config::TEMP_PATH` (resolves to `/tmp/senator_sperma`) -- **TTS**: piper-rs in-process (no subprocess). `TtsState` in `Data` holds `OnceLock>` + `DashMap` cache. Synthesize via `data.tts.synthesize(text)`. Pre-generates 6 event phrases per user on first voice event. No Google TTS fallback. +- **Permissions**: admin commands use `required_permissions`; recording/cron/trigger are `guild_only` and scoped by `guild_id` in the DB +- **Temp path**: always use `crate::config::temp_path()` (initialized with mode `0700`; never hardcode `/tmp/senator_sperma`) +- **Outbound URLs**: user-supplied `http(s)` URLs go through `crate::net::ensure_public_http_url` unless they belong to the configured Copyparty origin +- **TTS**: piper-rs in-process (no subprocess). `TtsState` holds `OnceLock>>` + a bounded `DashMap` cache. Synthesize via `data.tts.synthesize(text)`; work runs on `spawn_blocking`. No external TTS fallback. +- **Recording**: opt-in (`RECORDING_ENABLED=false` by default), one buffer per guild. ## Build - `build.rs` — compiles C stubs for espeak-rs-sys missing sonic/audio_object symbols -## Stubs to Fill (see COMPARISON.md) -- `commands/utilities.rs` — `/compress`, `/convert`, `/download` upload part -- `tasks/mod.rs` — cron execution bodies are `info!` only, no actual action - ## Database Tables -`settings` (key/value), `voice_activity`, `cron_jobs`, `triggers`, `trigger_keywords`, `trigger_responses` +`settings`, `voice_activity`, `user_stats`, `cron_jobs`, `triggers`, `trigger_keywords`, `trigger_responses` ## Commands -`/senator`, `/cron` (6), `/math`, `/music` (5: grajcuj, skip, queue, stop, join), `/qr` (2), `/trigger` (6), `/tts`, `/log_volume`, `/compress`, `/convert`, `/upload`, `/search`, `/download`, `/say`, `/web`, `/record`, `/leaderboard` +`/senator`, `/cron` (6), `/leaderboard` (5), `/math`, `/music` (5: grajcuj, skip, queue, stop, join), `/qr` (2), `/trigger` (6), `/tts`, `/record`, `/compress`, `/convert`, `/upload`, `/search`, `/download`, `/say`, `/web`. +Context menus: "Odtwórz", "Wyślij załączniki", "kompresja", "Pobierz z linku". diff --git a/CONTEXT.md b/CONTEXT.md index 278420f..5a992ec 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -10,8 +10,8 @@ holds no implementation decisions or specs (those live in `docs/adr/`). Despite the name, it contains **no audio** — only timestamps and durations. - **Recording** — capturing the *audio* of a voice channel into a rolling in-memory buffer while the bot is connected. Distinct from *voice activity* - (metadata). A bot "records" continuously while in a VC (unless opted out) and - "flushes" on `/record`. + (metadata). Recording is opt-in via `RECORDING_ENABLED` (default false); when + enabled a bot records continuously while in a VC and "flushes" on `/record`. - **Window** — the retention horizon of the recording buffer: how many seconds of the recent past are kept. Older audio is discarded as new audio arrives. Configured via `RECORDING_WINDOW_SECONDS` (default 900 = 15 minutes). @@ -40,10 +40,11 @@ holds no implementation decisions or specs (those live in `docs/adr/`). at the model's native sample rate (typically 22050 Hz mono), resampled to 48 kHz stereo via inline linear interpolation before playback. Songbird does not resample — playing un-resampled audio results in wrong playback speed. -- **TtsState** — struct held in poise `Data`: `OnceCell>` for the - model instance, `DashMap, Instant)>` for the text→audio cache. - Piper loaded lazily on first TTS call. Cache keyed by full phrase string, - evicted by TTL. No Google TTS fallback. +- **TtsState** — struct held in poise `Data`: `OnceLock>>` + for the model instance, `DashMap, Instant)>` for the + text→audio cache. Piper is loaded lazily on first TTS call, off the async + runtime. Cache is keyed by the full phrase, bounded by entries/bytes, and + evicted by TTL. No external TTS fallback. - **Pre-generation** — on first voice event for a user, synthesize all 6 event phrases (join/leave/deafen/undeafen/mute/unmute) with their display name and cache them. ~120ms first time, instant after. Lazy — no startup cost. diff --git a/README.md b/README.md index a3da394..f314793 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ The following binaries must be available on `PATH`: - `imagemagick` (`magick`) — image compression and conversion for `/compress` and `/convert` - `qalc` (Qalculate) — used by `/math` to evaluate expressions - `yt-dlp` (optional) — used by `/download` -- `piper` (optional) — local TTS for `/tts` and `/senator ... tts_vc`; falls back to Google Translate TTS if unavailable +- `piper` model files (optional) — local TTS for `/tts` and `/senator ... tts_vc`; TTS commands fail when unset Unlike the Python version, QR encode/decode is handled by the pure-Rust `qrcode`/`rxing` crates — no `zbar` system library required. @@ -48,8 +48,8 @@ created automatically on first run. For local TTS, set `TTS_MODEL_PATH` to a Piper `.onnx` model file (or a directory of models, in which case `TTS_VOICE` selects the model, defaulting to -`pl_PL-bass-high`). When `TTS_MODEL_PATH` is unset or Piper is unavailable, the -bot falls back to Google Translate TTS. +`pl_PL-bass-high`). There is no external TTS fallback: TTS commands fail when +the model is missing. ### 5. Run @@ -132,7 +132,7 @@ servers. - `/music join` — join the caller's voice channel ### TTS -- `/tts` — local Piper TTS (Polish, Google Translate fallback), sent as a file or played in voice +- `/tts` — local Piper TTS (Polish), sent as a file or played in voice ### Search & Web - `/web` — DuckDuckGo HTML scrape (soon to use 4get) @@ -148,6 +148,8 @@ servers. - `/say` — repeat the given text as the bot - `/math` — evaluate an ASCII math expression through `qalc` - `/qr generowanie` / `/qr dekodowanie` — QR encode / decode +- `/record` — upload the last recording window (requires `MANAGE_GUILD`) +- `/leaderboard` — `me`, `messages`, `active`, `afk`, `total` ### Triggers - `/trigger create` — new trigger group @@ -168,6 +170,7 @@ servers. ### Message Context Menu - "Wyślij załączniki" (right-click message) — upload attachments to Copyparty - "kompresja" (right-click message) — compress all attachments +- "Pobierz z linku" (right-click message) — download every URL in the message ## Architecture @@ -181,10 +184,12 @@ in-memory trigger cache, autocomplete cache, and per-guild music queue. - `src/copyparty/` — Copyparty search/upload client - `src/openrouter/` — OpenRouter chat client - `src/database/` — sqlx pool, migrations, query helpers (6 tables) -- `src/voice/` — `VoiceManager`, mp3 → songbird input +- `src/voice/` — `VoiceManager`, ffmpeg/symphonia → songbird input +- `src/net.rs` — blocks private/link-local hosts for user-supplied URLs +- `src/recording/` — per-guild rolling PCM buffer for `/record` - `src/events/` — `handle_ready`, voice-state, message (trigger matching) -- `src/tasks/` — 30s cron scheduler poll loop +- `src/tasks/` — 60s cron scheduler poll loop - `src/lang.rs` — user-facing Polish strings -All temporary files go through `crate::config::TEMP_PATH` -(`/tmp/senator_sperma`). +All temporary files go through `crate::config::temp_path()` +(`$TMPDIR/senator_sperma`, created with mode `0700` at startup). diff --git a/src/config.rs b/src/config.rs index c05c3ae..09eabeb 100644 --- a/src/config.rs +++ b/src/config.rs @@ -75,7 +75,7 @@ pub struct Config { pub llm_api_url: String, pub database_path: String, /// Optional path to the Piper TTS `.onnx` model (or a directory of models). - /// When unset, TTS falls back to Google Translate TTS. + /// When unset, TTS commands fail; there is no external fallback. pub tts_model_path: Option, /// Piper voice name, used to resolve the model file when `tts_model_path` /// points at a directory. Ignored when it points at a `.onnx` file directly. @@ -168,7 +168,7 @@ const ENV_VARS: &[EnvVar] = &[ name: "TTS_MODEL_PATH", required: false, default: None, - description: "Path to a Piper TTS .onnx model (or a models directory). Unset => Google TTS fallback", + description: "Path to a Piper TTS .onnx model (or a models directory); TTS is disabled when unset", }, EnvVar { name: "TTS_VOICE", @@ -284,11 +284,11 @@ impl Config { } } - /// Resolve the Piper model path to pass to the `piper` CLI. + /// Resolve the Piper model path passed to `piper-rs` (in-process). /// /// - `TTS_MODEL_PATH` set to a `.onnx` file => used verbatim. /// - `TTS_MODEL_PATH` set to a directory => `{dir}/{TTS_VOICE}.onnx`. - /// - `TTS_MODEL_PATH` unset => `None` (caller falls back to Google TTS). + /// - `TTS_MODEL_PATH` unset => `None` (TTS is unavailable). pub fn piper_model_path(&self) -> Option { let configured = self.tts_model_path.as_ref()?; let path = std::path::PathBuf::from(configured); -- cgit v1.2.3