Skip to content

Repository files navigation

MeetCap

MeetCap is a self-hosted Discord meeting assistant for a small internal development team. The MVP target is a Rust Discord bot plus a separate transcription worker, with local transcription through whisper.cpp.

This repository is a public sample snapshot of MVP-1 for a YouTube demo by @ClickBober. It shows a self-hosted MeetCap Discord bot solution using Gitea and a private Docker registry as the DevOps platform.

Current repository state: the Rust workspace includes shared domain and worker contract types, a Serenity/Songbird-based Discord bot command foundation, SQLite/file-backed meeting session metadata, voice recording and audio normalization, a worker HTTP API, a gated whisper-cli execution boundary, and worker-generated summary posting back through the bot. Docker-backed real model validation remains the main active quality task.

Repository Layout

.
|-- apps/
|   |-- discord-bot/
|   |   |-- Cargo.toml
|   |   `-- src/
|   |       |-- lib.rs
|   |       `-- main.rs
|   `-- transcription-worker/
|       |-- Cargo.toml
|       `-- src/
|           |-- lib.rs
|           `-- main.rs
|-- crates/
|   |-- meetcap-domain/
|   |-- meetcap-storage/
|   `-- meetcap-worker-client/
|-- infra/
|   |-- docker/
|   |   `-- ci/
|   `-- docker-compose/
|-- docs/
|   |-- api_protocol_v1.md
|   |-- design.md
|   |-- handoff.md
|   |-- scope.md
|   |-- tracker.md
|   `-- methodology/
|-- Cargo.toml
|-- Cargo.lock
`-- README.md

Workspace Packages

Package Path Purpose
meetcap-discord-bot apps/discord-bot Discord Gateway, slash commands, voice coordination, recording orchestration, and future summary posting.
meetcap-transcription-worker apps/transcription-worker Internal worker HTTP API, transcription job state, whisper-cli STT orchestration, and future summary generation.
meetcap-domain crates/meetcap-domain Shared meeting/session/job/artifact types and state transition logic.
meetcap-storage crates/meetcap-storage Shared /data and session artifact path helpers for SQLite/file metadata.
meetcap-worker-client crates/meetcap-worker-client Bot-side DTOs for the internal worker API described in docs/api_protocol_v1.md.

Local Development Setup

  1. Install Rust, preferably through rustup.
  2. Use a Rust toolchain with edition 2024 support. The workspace minimum is Rust 1.85.
  3. Install native Opus build prerequisites for Songbird voice support:
    • macOS/Homebrew: brew install cmake pkg-config opus
    • Debian/Ubuntu: apt-get install -y cmake pkg-config libopus-dev
  4. Verify the toolchain:
    rustc --version
    cargo --version
  5. Check that the workspace compiles:
    cargo check --workspace

No Discord credentials or whisper.cpp model files are required for normal unit tests. FFmpeg is needed for bot audio normalization tests and local recording conversion. Local app binaries run from source with cargo; Docker Compose is reserved for external dependencies and VPS deployment.

Optional local whisper.cpp setup

T-011 uses Pattern 2 from docs/design.md: the MeetCap worker owns the HTTP API and shells out to whisper-cli. For normal tests, this backend stays disabled. Enable it only when you have a local whisper.cpp build and model file.

On macOS Apple Silicon, including an M3 Max development machine, the default local setup is a repo-local checkout under .local/meetcap/vendor/. Generated binaries and downloaded models stay outside committed source because .local/ is ignored.

brew install cmake ffmpeg
mkdir -p .local/meetcap/vendor .local/meetcap/models
git clone https://github.com/ggml-org/whisper.cpp.git .local/meetcap/vendor/whisper.cpp
cd .local/meetcap/vendor/whisper.cpp
cmake -B build
cmake --build build -j --config Release
sh ./models/download-ggml-model.sh base
cd -

If you already have another local whisper.cpp checkout, you can seed the repo-local checkout without network access:

mkdir -p .local/meetcap/vendor .local/meetcap/models
git clone /path/to/existing/whisper.cpp .local/meetcap/vendor/whisper.cpp
cp /path/to/existing/whisper.cpp/models/ggml-base.bin .local/meetcap/models/ggml-base.bin

If cmake --build fails on macOS with errors such as array/mutex/cstdio not found, this Command Line Tools install is missing the default libc++ header search path. Reconfigure with the SDK include path explicitly:

cmake -B build \
  "-DCMAKE_OSX_SYSROOT=/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk" \
  "-DCMAKE_CXX_FLAGS=-isystem /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/c++/v1"
cmake --build build -j --config Release

Use base, not base.en, because MeetCap needs Ukrainian, Russian, and English. The model download above creates:

.local/meetcap/vendor/whisper.cpp/models/ggml-base.bin

You can either point MeetCap directly at that file, or copy/symlink it into the shared local model directory:

cp .local/meetcap/vendor/whisper.cpp/models/ggml-base.bin .local/meetcap/models/ggml-base.bin

Quick upstream sanity check:

.local/meetcap/vendor/whisper.cpp/build/bin/whisper-cli \
  -m .local/meetcap/models/ggml-base.bin \
  -f .local/meetcap/vendor/whisper.cpp/samples/jfk.wav \
  -l auto \
  -t 8

Large model

.local/meetcap/vendor/whisper.cpp/build/bin/whisper-cli \
  -m .local/meetcap/vendor/whisper.cpp/models/ggml-large-v3.bin \
  -f media/sample_meeting_meeting_20260501.wav \
  -oj \
  -of media/sample_meeting_meeting_20260501.large-v3 \
  -l auto \
  -t 8 \
  -ng \
  -mc 0 \
  -bs 1 \
  -bo 1 \
  -ml 80
.local/meetcap/vendor/whisper.cpp/build/bin/whisper-cli \
  -m .local/meetcap/vendor/whisper.cpp/models/ggml-medium.bin \
  -f media/sample_meeting_meeting_20260501.wav \
  -oj \
  -of media/sample_meeting_meeting_20260501.medium \
  -l auto \
  -t 8 \
  -ng \
  -mc 0 \
  -bs 1 \
  -bo 1

If the command fails on macOS with a Metal buffer allocation error, keep the same build and disable GPU for local execution with -ng.

Then configure the MeetCap worker:

MEETCAP_DATA_ROOT=.local/meetcap/data
MEETCAP_SQLITE_PATH=.local/meetcap/data/meetcap.sqlite3
MEETCAP_WORKER_WHISPER_CLI_PATH=.local/meetcap/vendor/whisper.cpp/build/bin/whisper-cli
MEETCAP_WORKER_WHISPER_MODEL_PATH=.local/meetcap/models/ggml-base.bin
MEETCAP_WORKER_WHISPER_PROMPT=
MEETCAP_WORKER_WHISPER_VAD=true
MEETCAP_WORKER_WHISPER_VAD_MODEL_PATH=.local/meetcap/models/for-tests-silero-v6.2.0-ggml.bin
MEETCAP_WORKER_MODEL=base
MEETCAP_WORKER_WHISPER_THREADS=8
MEETCAP_WORKER_WHISPER_NO_GPU=true

MEETCAP_WORKER_WHISPER_THREADS=8 is a conservative local starting point for an M3 Max with 42 GB RAM. Benchmark 4, 8, and 12 before changing the default for shared docs or VPS deployment.

A local setup can keep .local/meetcap/whisper.env with these repo-local paths preconfigured for worker runs and gated model tests. The file uses plain dotenv syntax, compatible with infra/docker-compose/.env. Load it in both bot and worker shells with automatic export enabled so /meeting stop submits audio paths under the same MEETCAP_DATA_ROOT that the worker validates.

set -a
source .local/meetcap/whisper.env
set +a

For the gated real model integration test, use separate test variables:

MEETCAP_TEST_WHISPER_CLI=.local/meetcap/vendor/whisper.cpp/build/bin/whisper-cli
MEETCAP_TEST_WHISPER_MODEL=.local/meetcap/models/ggml-base.bin
MEETCAP_TEST_WHISPER_FIXTURE=.local/meetcap/vendor/whisper.cpp/samples/jfk.wav
MEETCAP_TEST_WHISPER_NO_GPU=true

Then load the file before running the test:

set -a
source .local/meetcap/whisper.env
set +a
cargo test -p meetcap-transcription-worker --test worker_whisper_backend_integration -- --nocapture

When these MEETCAP_TEST_WHISPER_* variables are unset, the test skips cleanly. The fixture must be a 16-bit WAV; convert other audio with:

mkdir -p .local/meetcap/fixtures
ffmpeg -i input.m4a -ar 16000 -ac 1 -c:a pcm_s16le .local/meetcap/fixtures/small-speech.wav

Docker-backed worker validation

T-024 validates the production-style transcription-worker container, not just the host cargo path. The validation stack builds apps/transcription-worker/Dockerfile, mounts the local model and data directories, starts the worker through infra/docker-compose/docker-compose.validation.yml, waits for GET /health/ready, submits a transcription job, polls GET /jobs/{jobId}, and verifies that transcript and summary artifacts were written under the mounted data root.

Prerequisites:

  • Docker and Docker Compose plugin are installed and working.
  • .local/meetcap/models/ggml-base.bin exists.
  • .local/meetcap/fixtures/small-speech.wav exists and is a mono 16 kHz PCM s16le WAV.

If you need to create the fixture:

mkdir -p .local/meetcap/fixtures
ffmpeg -i input.m4a -ar 16000 -ac 1 -c:a pcm_s16le .local/meetcap/fixtures/small-speech.wav

Run the validation:

bash infra/docker/ci/validate-worker-docker.sh

Expected flow:

  1. The script checks that the model and WAV fixture exist.
  2. It builds and starts the validation worker container on localhost:8080.
  3. It waits for GET /health/ready to return {"ok":true,...}.
  4. It copies the fixture into .local/meetcap/data/sessions/<meeting-id>/processed/recording.wav.
  5. It submits POST /jobs/transcribe with summary: true.
  6. It polls GET /jobs/{jobId} until the status becomes completed.
  7. It verifies:
    • .local/meetcap/data/sessions/<meeting-id>/transcript/transcript.json
    • .local/meetcap/data/sessions/<meeting-id>/summary/summary.md

Expected success output includes:

=== T-024: Docker-backed Worker Validation ===
Starting worker container...
Waiting for /health/ready...
Worker is ready!
Submitting transcription job...
Polling for job <job-id> completion...
Job completed successfully!
Verifying artifacts...
OK: Transcript found.
OK: Summary found.
=== Validation Successful! ===

If it fails:

  • Model not found means .local/meetcap/models/ggml-base.bin is missing.
  • Fixture not found means .local/meetcap/fixtures/small-speech.wav is missing.
  • Worker health check failed usually means the container did not start correctly; inspect docker compose -f infra/docker-compose/docker-compose.validation.yml logs.
  • Job failed means the worker accepted the request but whisper-cli or artifact generation failed inside the container.
  • Job timed out usually means the fixture is too large for the retry window or the worker is stalled; inspect container logs first.

Successful validation leaves evidence in the mounted host data directory under .local/meetcap/data/sessions/.

Build, Check, and Lint

Run a workspace compile check:

cargo check --workspace

Run formatting verification:

cargo fmt --all -- --check

Apply formatting:

cargo fmt --all

Run clippy with warnings treated as errors:

cargo clippy --workspace --all-targets -- -D warnings

Recommended pre-handoff command set:

cargo fmt --all -- --check
cargo check --workspace
cargo test --workspace
cargo clippy --workspace --all-targets -- -D warnings

CI Pipeline

The Gitea workflow lives at .gitea/workflows/main.yaml.

The pipeline is designed for a self-hosted Gitea repository. Sample endpoints:

https://meetcap.click:3443/MeetCapCompany/meet-cap-public.git
ssh://git@meetcap.click:2222/MeetCapCompany/meet-cap-public.git

It has two jobs:

Job Purpose
build-and-test Checks out the repository, builds or reuses the Docker CI image, then runs Rust quality gates in the container.
secret-scan Runs gitleaks in Docker and records a SARIF report.

The Docker CI image is defined in infra/docker/ci/Dockerfile.rust and currently uses rust:1.93-bookworm.

The quality-gate entrypoint is infra/docker/ci/run-tests.sh. It runs:

  • cargo fmt --all -- --check
  • cargo clippy --workspace --all-targets -- -D warnings
  • cargo check --workspace
  • cargo test --workspace --lib
  • CI-safe integration test discovery
  • cargo test --workspace
  • cargo llvm-cov --workspace --all-targets --cobertura
  • diff-cover with an 80% changed-lines threshold when a compare branch is available

The workspace now includes CI-safe integration tests under apps/discord-bot/tests/; live Discord smoke tests remain manual and are not part of normal CI.

Known false-positive example secrets are documented in .gitleaksignore. Do not add real secrets to that file; rotate and remove real leaked credentials instead.

Run Locally

Run the Discord bot against a test guild after putting real values in untracked infra/docker-compose/.env:

set -a
source infra/docker-compose/.env
set +a
cargo run -p meetcap-discord-bot

The bot reads MEETCAP_DISCORD_BOT_TOKEN, connects through Discord Gateway with Serenity, and registers the /meeting ... command tree. Use MEETCAP_DISCORD_TEST_GUILD_ID for guild-scoped command registration during development; otherwise commands are registered globally.

Run the transcription worker:

set -a
source infra/docker-compose/.env
set +a
cargo run -p meetcap-transcription-worker

If you keep Whisper settings separate instead of merging them into infra/docker-compose/.env, source .local/meetcap/whisper.env with the same set -a / set +a wrapper.

Expected output:

MeetCap transcription-worker listening on 127.0.0.1:8080

Without MEETCAP_WORKER_WHISPER_CLI_PATH and MEETCAP_WORKER_WHISPER_MODEL_PATH, the worker exposes the HTTP contract but keeps real STT execution disabled. With those variables set, accepted jobs are processed by whisper-cli.

Test

Run all tests:

cargo test --workspace

Run tests for one package:

cargo test -p meetcap-domain
cargo test -p meetcap-discord-bot
cargo test -p meetcap-transcription-worker

The current tests cover package wiring, shared domain state transitions, worker DTO JSON compatibility, and Discord command parsing/dispatch without live Discord API calls.

Local Infrastructure

Docker Compose files live under infra/docker-compose/.

File Purpose
docker-compose.yml Local/dev external dependencies only. It currently has no services because MVP local dev uses SQLite/file storage.
docker-compose.deploy.yml VPS runtime shape using prebuilt bot and worker images.
.env.example Redacted local template. Copy to untracked .env before real local Discord tests.
.env.deploy.example Redacted deploy template. Real deploy values must come from Ansible Vault or non-committed host vars.

Local dev with cargo:

cp infra/docker-compose/.env.example infra/docker-compose/.env
set -a
source infra/docker-compose/.env
set +a
cargo run -p meetcap-transcription-worker

Run the bot from a second shell after adding a real MEETCAP_DISCORD_BOT_TOKEN and sourcing the same env file:

set -a
source infra/docker-compose/.env
set +a
cargo run -p meetcap-discord-bot

The bot is now a long-running Gateway process when a valid Discord token is provided. The worker is a long-running HTTP process; real transcription runs only when the whisper-cli binary and model paths are configured. When future external services such as PostgreSQL, Redis, or Kafka enter scope, add them to the local Compose file and start them before running cargo.

VPS providers

  • hostinger KVM-4 (4 vCPU, 16 GB RAM, 200 GB NVMe), KVM-8(8 vCPU, 32 GB RAM, 400 GB NVMe). Get 20% Discount using this referral link: Referral Link

Deployment Skeleton

Ansible files live under infra/ansible/.

Path Purpose
ansible.cfg Inventory and role path defaults.
inventory/production.ini.example Redacted production inventory template.
playbooks/provision.yml Installs Docker/Compose and prepares the deploy directory.
playbooks/check-docker-setup.yml Verifies Docker and Compose on the VPS.
playbooks/deploy.yml Stages deploy Compose/env artifacts and can apply the stack when real vars are present.
roles/docker Docker/Compose bootstrap role.
roles/meetcap MeetCap deploy artifact and Compose role.

Real inventories, Vault files, and host-specific vars are intentionally untracked.

Runtime Configuration

The selected MVP STT pattern is: MeetCap transcription-worker shells out to whisper-cli. Local cargo uses host paths; VPS/Docker should use fixed container paths such as /usr/local/bin/whisper-cli and /models/ggml-base.bin.

meetcap-discord-bot

Variable Required Description
MEETCAP_DISCORD_BOT_TOKEN Yes Discord bot token for live Gateway usage.
MEETCAP_DISCORD_TEST_GUILD_ID No Test guild snowflake for guild-scoped command registration during development.
MEETCAP_WORKER_BASE_URL No, later Worker base URL. Local cargo uses http://127.0.0.1:8080; deploy Compose uses http://transcription-worker:8080.
MEETCAP_WORKER_EXTERNAL_BASE_URL No Optional external/public base URL used only for bot-rendered transcript links when the worker returned relative download paths. Example: http://demo.example.invalid:5580.
MEETCAP_DATA_ROOT No Shared data root for session metadata files, default /data.
MEETCAP_SQLITE_PATH No SQLite metadata database path, default {MEETCAP_DATA_ROOT}/meetcap.sqlite3.
MEETCAP_ALLOWED_ROLE_IDS Yes for meeting commands Comma-separated Discord role IDs allowed to run meeting commands. Empty means deny.
MEETCAP_ALLOWED_VOICE_CHANNEL_IDS Yes for /meeting start Comma-separated Discord voice channel IDs allowed for recording. Empty means deny start.
MEETCAP_ALLOWED_TEXT_CHANNEL_IDS Yes for /meeting start Comma-separated Discord text channel IDs allowed for status/output references. Empty means deny start.
RUST_LOG No, later Rust log filter, for example info or meetcap=debug.

meetcap-transcription-worker

Variable Required Description
MEETCAP_WORKER_BIND_ADDR No, later Worker HTTP bind address, expected default 0.0.0.0:8080.
MEETCAP_WORKER_API_TOKEN Conditional, later Bearer token if the internal worker API is exposed outside a private network.
MEETCAP_WORKER_ARTIFACT_BASE_URL No Optional public base URL used to prefix safe transcript download paths. If unset, worker responses use relative internal API paths.
MEETCAP_DATA_ROOT No Shared data root, expected default /data.
MEETCAP_SQLITE_PATH No SQLite metadata database path.
MEETCAP_WORKER_MODEL No Model label reported in worker API responses, for example base.
MEETCAP_WORKER_WHISPER_CLI_PATH Yes for real STT Path to the whisper-cli executable. Local example: /absolute/path/to/whisper.cpp/build/bin/whisper-cli; Docker example: /usr/local/bin/whisper-cli.
MEETCAP_WORKER_WHISPER_MODEL_PATH Yes for real STT Path to the configured whisper.cpp model file. Local example: /absolute/path/to/models/ggml-base.bin; Docker example: /models/ggml-base.bin.
MEETCAP_WORKER_WHISPER_PROMPT No Optional one-line whisper-cli --prompt value. Leave unset by default for mixed-language meetings; enable only when you explicitly want decoder bias toward a glossary or domain wording.
MEETCAP_WORKER_WHISPER_VAD No Boolean VAD switch. Set false to disable VAD even if a Silero model is present, true to force-enable VAD and allow model autodiscovery, or leave unset to preserve the legacy autodiscovery behavior.
MEETCAP_WORKER_WHISPER_VAD_MODEL_PATH No Optional Silero VAD model path. Used when VAD is enabled; if omitted, the worker auto-enables a known Silero model found next to the Whisper model.
MEETCAP_WORKER_WHISPER_THREADS No CPU thread count for whisper-cli, for example 4.
MEETCAP_WORKER_WHISPER_NO_GPU No Set true to pass -ng to whisper-cli and force CPU/Accelerate execution. Useful on macOS if Metal allocation fails.
RUST_LOG No Rust log filter, for example info or meetcap=debug.

Secrets must not be committed. Keep real values in untracked infra/docker-compose/.env, untracked infra/docker-compose/.env.deploy, Ansible Vault, or host-specific inventory variables.

Documentation Map

Read these before starting implementation work:

Document Purpose
docs/handoff.md Current session state, recent changes, risks, and next steps.
docs/scope.md Product boundaries, milestones, success metrics, dependencies, and non-goals.
docs/design.md Architecture, service boundaries, test requirements, deployment approach, and risks.
docs/api_protocol_v1.md Internal bot-to-worker HTTP protocol for transcription and summaries.
docs/bot_user_manual.md Bot setup, user journeys, and manual/automation use-case requirements.
docs/tracker.md Task source of truth with acceptance criteria and status.
docs/methodology/methodology.md Process gates, Definition of Done, testing, security, CI, and handoff rules.

Contributor Workflow

  1. See docs/design.md and docs/tracker.md for the implementation order.
  2. Pick the next tracker task or confirm the intended task with the maintainer.
  3. Keep changes scoped to the tracker task and referenced design section.
  4. Prefer small slices that can be validated in less than one day.
  5. Add or update tests for nontrivial logic.
  6. Run the local validation commands before handing off.
  7. Update docs/tracker.md with status and evidence.
  8. Update docs/handoff.md using the canonical schema from docs/methodology/methodology.md.

Security and Secrets

  • Never commit Discord tokens, worker API tokens, model credentials, VPS credentials, or Ansible Vault passwords.
  • Keep local secrets in untracked files.
  • Prefer redacted committed templates such as .env.example once infra exists.
  • CI runs gitleaks for secret scanning; keep known false-positive examples in .gitleaksignore only when they are documented non-secrets.

Releases

Packages

Contributors

Languages