A standalone meeting agent API & CLI for transcribing and summarizing meeting recordings.
services/meeting-bot; clients use POST /bots on the agent onlymeeting-agent config editPUT /config endpoints/docs, spec at /api-docs/openapi.json~/.meeting-agent/ directoryMeetily / clients ──► meeting-agent-server :8080
│ POST /import, /meetings, /bots (proxy)
├─► whisperx / diarize / minutes-llm
└─► meeting-bot :8091 (internal)
join Teams → local WAV → POST /import
Rust workspace crates:
meeting-agent-core: Shared business logic, models, storage, bot client, orchestratormeeting-agent-server: Axum HTTP API + OpenAPImeeting-agent-cli: Command-line interface and API clientmeeting-agent-mcp: stdio MCP CLI that wraps the REST APImeeting-agent-mcp-server: HTTP MCP server that wraps the REST APINode worker (not in Cargo workspace):
services/meeting-bot: Bun + Elysia + Playwright; SQLite job rows + local recordings# Clone the repository
git clone https://github.com/bmw-ece-ntust/ai-meeting-agent.git
cd ai-meeting-agent
# Build all binaries
cargo build --release
# Binaries will be in target/release/
# - meeting-agent-server
# - meeting-agent
# - meeting-agent-mcp
# - meeting-agent-mcp-server
Copy .env.example to .env and configure:
cp .env.example .env
Key environment variables:
# Server
MEETING_AGENT_PORT=8080
MEETING_AGENT_HOST=127.0.0.1
# Transcription (choose one provider)
TRANSCRIPTION_PROVIDER=openai
TRANSCRIPTION_API_KEY=your-api-key-here
TRANSCRIPTION_BASE_URL=https://api.openai.com/v1
TRANSCRIPTION_MODEL=whisper-1
# Summary
SUMMARY_PROVIDER=openai
SUMMARY_API_KEY=your-api-key-here
SUMMARY_BASE_URL=https://api.openai.com/v1
SUMMARY_MODEL=gpt-4o-mini
SUMMARY_TEMPERATURE=0.3
SUMMARY_MAX_TOKENS=1024
SUMMARY_LANGUAGE=en
# Using the CLI (default port 8080, host 127.0.0.1)
meeting-agent server
# Custom port and host
meeting-agent server --port 3000 --host 0.0.0.0
# Or run the server binary directly
meeting-agent-server
Once the server is running, open:
http://127.0.0.1:8080/docshttp://127.0.0.1:8080/api-docs/openapi.json# Import a meeting recording (with optional title)
meeting-agent import meeting.wav --title "Q3 Planning"
meeting-agent import recording.mp3
# List all meetings
meeting-agent list
# Show meeting details (8-char ID prefix supported)
meeting-agent show abc12345
# Generate summary (templates: full, key-points, action-items, decisions)
meeting-agent summarize abc12345 --template key-points
meeting-agent summarize abc12345 --template action-items --language en
# Export transcript (formats: srt, vtt, text, json)
meeting-agent export abc12345 --format srt
meeting-agent export abc12345 --format json --output transcript.json
# Manage configuration
meeting-agent config show
meeting-agent config set transcription.provider openai
meeting-agent config set server.port 3000
meeting-agent config set diarize.enabled true
# Interactive config wizard (guided setup)
meeting-agent config edit
# Health check
curl http://127.0.0.1:8080/health
# List meetings
curl -H "X-API-Key: your-key" http://127.0.0.1:8080/meetings
# Import audio file
curl -X POST -H "X-API-Key: your-key" \
-F "file=@meeting.mp3" -F "title=Q3 Planning" \
http://127.0.0.1:8080/import
# Check job status
curl -H "X-API-Key: your-key" http://127.0.0.1:8080/jobs/{job_id}/status
# Generate summary
curl -X POST -H "X-API-Key: your-key" \
http://127.0.0.1:8080/meetings/{id}/summary
# Get current config (secrets masked)
curl -H "X-API-Key: your-key" http://127.0.0.1:8080/config
# Update transcription config
curl -X PUT -H "X-API-Key: your-key" -H "Content-Type: application/json" \
-d '{"provider":"groq","base_url":"https://api.groq.com/openai/v1","model":"whisper-large-v3","chunk_seconds":600,"chunk_concurrency":2}' \
http://127.0.0.1:8080/config/transcription
GET /health - Health checkGET /version - Version infoGET /meetings - List all meetingsGET /meetings/{id} - Get meeting detailsPOST /meetings - Create meetingPATCH /meetings/{id} - Update meetingDELETE /meetings/{id} - Delete meetingGET /meetings/{id}/transcript - Get transcriptGET /meetings/{id}/summary - List all summaries for a meetingPOST /meetings/{id}/summary - Generate summary (templates: key_points, action_items, decisions, full)GET /meetings/{id}/summary/{template} - Get specific summaryPOST /import - Import audio fileGET /jobs/{job_id}/status - Check job statusGET /jobs/{job_id}/events - SSE stream of job progressPOST /jobs/{job_id}/cancel - Cancel a running jobGET /config - Get current config (secrets masked as ****)PUT /config - Update full config (validates before saving)GET /config/transcription - Get transcription configPUT /config/transcription - Update transcription configGET /config/summary - Get summary configPUT /config/summary - Update summary configSecret handling: API keys are masked (
****) in GET responses. To keep an existing key unchanged, send"****"in PUT requests. To replace, send the new key value.
GET /docs - Swagger UI (interactive API docs)GET /api-docs/openapi.json - OpenAPI 3.0 specAll data is stored in ~/.meeting-agent/:
~/.meeting-agent/
├── config.json
└── meetings/{id}/
├── meeting.json
├── audio/
│ └── {original-filename}
├── transcript.json
└── summaries/
├── key_points.json
├── action_items.json
├── decisions.json
└── full.json
Build server, diarize, and meeting-bot images:
./deploy/docker-build.sh
# DGX / arm64: PLATFORM=linux/arm64 ./deploy/docker-build.sh
cp deploy/.env.example deploy/.env
# MEETING_BOT_ENABLED=true (default in compose)
docker compose -f deploy/docker-compose.yml --env-file deploy/.env up -d
http://127.0.0.1:8080 (Meetily / curl)http://meeting-bot:8091 (do not point the UI at it)POST /bots with { "platform":"teams", "meeting_url":"…" } — see docs/API.mdai-meeting-agent/
├── Cargo.toml # Workspace root
├── crates/
│ ├── core/ # business logic, models, storage, bots client
│ ├── server/ # Axum HTTP API + OpenAPI
│ ├── cli/ # CLI
│ ├── mcp/ # MCP
│ └── diarize-service/ # remote diarize microservice
├── services/
│ └── meeting-bot/ # Bun live join/record worker (Teams v1)
├── deploy/ # docker-compose, docker-build.sh, Dockerfiles
├── docs/
│ └── API.md
└── README.md
| Crate | Key Dependencies |
|---|---|
meeting-agent-core | axum, tokio, serde, reqwest, uuid, chrono, anyhow, thiserror, dirs, ffmpeg-sidecar, speakrs, symphonia |
meeting-agent-server | axum, tower-http (cors, trace, compression-gzip), utoipa, utoipa-swagger-ui |
meeting-agent-cli | clap, colored, indicatif, comfy-table, dialoguer |
$ cargo check --workspace
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.46s
All four crates compile cleanly with no warnings or errors.
# Run tests
cargo test
# Check code
cargo check --workspace
# Format code
cargo fmt
# Lint
cargo clippy
Speaker diarization runs in-process via speakrs,
a Rust-native pyannote community-1 style pipeline (segmentation + embedding +
VBx clustering). No separate server or Python runtime is required — the first
import with diarize.enabled=true loads the model once and caches it for the
process lifetime.
meeting-agent config set diarize.enabled true
By default, meeting-agent automatically detects and uses GPU acceleration for
speaker diarization when available, with graceful fallback to CPU:
If GPU initialization fails (missing drivers, insufficient memory, etc.), the system logs a warning and automatically falls back to CPU mode. No manual configuration is required.
| Mode | Backend | Use it for |
|---|---|---|
auto (default) | Platform-specific GPU priority | Automatic GPU detection with CPU fallback |
cpu | ONNX Runtime CPU | Portable, widest compatibility |
coreml | Native CoreML | macOS with CoreML acceleration |
coreml-fast | Native CoreML (2s step) | macOS, faster on long meetings |
cuda | ONNX Runtime CUDA | NVIDIA GPU |
cuda-fast | ONNX Runtime CUDA (2s step) | NVIDIA GPU, faster on long meetings |
migraphx | ONNX Runtime MIGraphX | AMD GPU |
To override automatic detection and force a specific mode:
meeting-agent config set diarize.execution_mode cpu
With the default online feature, speakrs downloads models on first use
from avencera/speakrs-models
to a local cache. Set DIARIZE_MODEL_DIR to point at a pre-bundled model
directory for offline/airgapped setups:
meeting-agent config set diarize.model_dir /opt/speakrs-models
| Variable | Default | Description |
|---|---|---|
DIARIZE_ENABLED | false | Enable speaker diarization during import |
DIARIZE_EXECUTION_MODE | auto | auto | cpu | coreml | coreml-fast | cuda | cuda-fast | migraphx |
DIARIZE_MODEL_DIR | (blank) | Local model dir; blank = download on first use |
ffmpeg not foundAudio conversion and chunking require ffmpeg + ffprobe on your PATH:
# macOS
brew install ffmpeg
# Ubuntu/Debian
sudo apt install ffmpeg
Check TRANSCRIPTION_API_KEY is set and valid:
meeting-agent config show
# Verify api_key field is not "(not set)"
Long audio is auto-chunked. Adjust chunk settings:
meeting-agent config set transcription.chunk_seconds 300
meeting-agent config set transcription.chunk_concurrency 4
Ensure diarize.enabled is true and the execution mode is valid for
your platform:
meeting-agent config set diarize.enabled true
meeting-agent config show
The first import with diarization enabled downloads the speakrs models
(~hundreds of MB) on first use; subsequent imports reuse the cached
pipeline. For offline setups, point diarize.model_dir at a pre-bundled
model directory.
The config file (~/.meeting-agent/config.json) is created with chmod 600
(owner read/write only). If permissions are wrong:
chmod 600 ~/.meeting-agent/config.json
Delete the config file and run any command — a fresh default config is auto-created:
rm ~/.meeting-agent/config.json
meeting-agent config show
Create /etc/systemd/system/meeting-agent.service:
[Unit]
Description=Meeting Agent API Server
After=network.target
[Service]
Type=simple
User=meeting
WorkingDirectory=/opt/meeting-agent
EnvironmentFile=/opt/meeting-agent/.env
ExecStart=/opt/meeting-agent/meeting-agent-server
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now meeting-agent
FROM rust:1.70-slim as builder
WORKDIR /app
COPY . .
RUN apt-get update && apt-get install -y ffmpeg && cargo build --release
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ffmpeg && rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/target/release/meeting-agent-server /usr/local/bin/
EXPOSE 8080
CMD ["meeting-agent-server"]
docker build -t meeting-agent .
docker run -p 8080:8080 -v ~/.meeting-agent:/root/.meeting-agent meeting-agent
server {
listen 80;
server_name meetings.example.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# SSE support (for /jobs/{id}/events)
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 86400;
}
}
MIT
BMW ECE NTUST
200 commits
Rust
91.5%
TypeScript
8.0%
A standalone meeting agent API & CLI for transcribing and summarizing meeting recordings.
services/meeting-bot; clients use POST /bots on the agent onlymeeting-agent config editPUT /config endpoints/docs, spec at /api-docs/openapi.json~/.meeting-agent/ directoryMeetily / clients ──► meeting-agent-server :8080
│ POST /import, /meetings, /bots (proxy)
├─► whisperx / diarize / minutes-llm
└─► meeting-bot :8091 (internal)
join Teams → local WAV → POST /import
Rust workspace crates:
meeting-agent-core: Shared business logic, models, storage, bot client, orchestratormeeting-agent-server: Axum HTTP API + OpenAPImeeting-agent-cli: Command-line interface and API clientmeeting-agent-mcp: stdio MCP CLI that wraps the REST APImeeting-agent-mcp-server: HTTP MCP server that wraps the REST APINode worker (not in Cargo workspace):
services/meeting-bot: Bun + Elysia + Playwright; SQLite job rows + local recordings# Clone the repository
git clone https://github.com/bmw-ece-ntust/ai-meeting-agent.git
cd ai-meeting-agent
# Build all binaries
cargo build --release
# Binaries will be in target/release/
# - meeting-agent-server
# - meeting-agent
# - meeting-agent-mcp
# - meeting-agent-mcp-server
Copy .env.example to .env and configure:
cp .env.example .env
Key environment variables:
# Server
MEETING_AGENT_PORT=8080
MEETING_AGENT_HOST=127.0.0.1
# Transcription (choose one provider)
TRANSCRIPTION_PROVIDER=openai
TRANSCRIPTION_API_KEY=your-api-key-here
TRANSCRIPTION_BASE_URL=https://api.openai.com/v1
TRANSCRIPTION_MODEL=whisper-1
# Summary
SUMMARY_PROVIDER=openai
SUMMARY_API_KEY=your-api-key-here
SUMMARY_BASE_URL=https://api.openai.com/v1
SUMMARY_MODEL=gpt-4o-mini
SUMMARY_TEMPERATURE=0.3
SUMMARY_MAX_TOKENS=1024
SUMMARY_LANGUAGE=en
# Using the CLI (default port 8080, host 127.0.0.1)
meeting-agent server
# Custom port and host
meeting-agent server --port 3000 --host 0.0.0.0
# Or run the server binary directly
meeting-agent-server
Once the server is running, open:
http://127.0.0.1:8080/docshttp://127.0.0.1:8080/api-docs/openapi.json# Import a meeting recording (with optional title)
meeting-agent import meeting.wav --title "Q3 Planning"
meeting-agent import recording.mp3
# List all meetings
meeting-agent list
# Show meeting details (8-char ID prefix supported)
meeting-agent show abc12345
# Generate summary (templates: full, key-points, action-items, decisions)
meeting-agent summarize abc12345 --template key-points
meeting-agent summarize abc12345 --template action-items --language en
# Export transcript (formats: srt, vtt, text, json)
meeting-agent export abc12345 --format srt
meeting-agent export abc12345 --format json --output transcript.json
# Manage configuration
meeting-agent config show
meeting-agent config set transcription.provider openai
meeting-agent config set server.port 3000
meeting-agent config set diarize.enabled true
# Interactive config wizard (guided setup)
meeting-agent config edit
# Health check
curl http://127.0.0.1:8080/health
# List meetings
curl -H "X-API-Key: your-key" http://127.0.0.1:8080/meetings
# Import audio file
curl -X POST -H "X-API-Key: your-key" \
-F "file=@meeting.mp3" -F "title=Q3 Planning" \
http://127.0.0.1:8080/import
# Check job status
curl -H "X-API-Key: your-key" http://127.0.0.1:8080/jobs/{job_id}/status
# Generate summary
curl -X POST -H "X-API-Key: your-key" \
http://127.0.0.1:8080/meetings/{id}/summary
# Get current config (secrets masked)
curl -H "X-API-Key: your-key" http://127.0.0.1:8080/config
# Update transcription config
curl -X PUT -H "X-API-Key: your-key" -H "Content-Type: application/json" \
-d '{"provider":"groq","base_url":"https://api.groq.com/openai/v1","model":"whisper-large-v3","chunk_seconds":600,"chunk_concurrency":2}' \
http://127.0.0.1:8080/config/transcription
GET /health - Health checkGET /version - Version infoGET /meetings - List all meetingsGET /meetings/{id} - Get meeting detailsPOST /meetings - Create meetingPATCH /meetings/{id} - Update meetingDELETE /meetings/{id} - Delete meetingGET /meetings/{id}/transcript - Get transcriptGET /meetings/{id}/summary - List all summaries for a meetingPOST /meetings/{id}/summary - Generate summary (templates: key_points, action_items, decisions, full)GET /meetings/{id}/summary/{template} - Get specific summaryPOST /import - Import audio fileGET /jobs/{job_id}/status - Check job statusGET /jobs/{job_id}/events - SSE stream of job progressPOST /jobs/{job_id}/cancel - Cancel a running jobGET /config - Get current config (secrets masked as ****)PUT /config - Update full config (validates before saving)GET /config/transcription - Get transcription configPUT /config/transcription - Update transcription configGET /config/summary - Get summary configPUT /config/summary - Update summary configSecret handling: API keys are masked (
****) in GET responses. To keep an existing key unchanged, send"****"in PUT requests. To replace, send the new key value.
GET /docs - Swagger UI (interactive API docs)GET /api-docs/openapi.json - OpenAPI 3.0 specAll data is stored in ~/.meeting-agent/:
~/.meeting-agent/
├── config.json
└── meetings/{id}/
├── meeting.json
├── audio/
│ └── {original-filename}
├── transcript.json
└── summaries/
├── key_points.json
├── action_items.json
├── decisions.json
└── full.json
Build server, diarize, and meeting-bot images:
./deploy/docker-build.sh
# DGX / arm64: PLATFORM=linux/arm64 ./deploy/docker-build.sh
cp deploy/.env.example deploy/.env
# MEETING_BOT_ENABLED=true (default in compose)
docker compose -f deploy/docker-compose.yml --env-file deploy/.env up -d
http://127.0.0.1:8080 (Meetily / curl)http://meeting-bot:8091 (do not point the UI at it)POST /bots with { "platform":"teams", "meeting_url":"…" } — see docs/API.mdai-meeting-agent/
├── Cargo.toml # Workspace root
├── crates/
│ ├── core/ # business logic, models, storage, bots client
│ ├── server/ # Axum HTTP API + OpenAPI
│ ├── cli/ # CLI
│ ├── mcp/ # MCP
│ └── diarize-service/ # remote diarize microservice
├── services/
│ └── meeting-bot/ # Bun live join/record worker (Teams v1)
├── deploy/ # docker-compose, docker-build.sh, Dockerfiles
├── docs/
│ └── API.md
└── README.md
| Crate | Key Dependencies |
|---|---|
meeting-agent-core | axum, tokio, serde, reqwest, uuid, chrono, anyhow, thiserror, dirs, ffmpeg-sidecar, speakrs, symphonia |
meeting-agent-server | axum, tower-http (cors, trace, compression-gzip), utoipa, utoipa-swagger-ui |
meeting-agent-cli | clap, colored, indicatif, comfy-table, dialoguer |
$ cargo check --workspace
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.46s
All four crates compile cleanly with no warnings or errors.
# Run tests
cargo test
# Check code
cargo check --workspace
# Format code
cargo fmt
# Lint
cargo clippy
Speaker diarization runs in-process via speakrs,
a Rust-native pyannote community-1 style pipeline (segmentation + embedding +
VBx clustering). No separate server or Python runtime is required — the first
import with diarize.enabled=true loads the model once and caches it for the
process lifetime.
meeting-agent config set diarize.enabled true
By default, meeting-agent automatically detects and uses GPU acceleration for
speaker diarization when available, with graceful fallback to CPU:
If GPU initialization fails (missing drivers, insufficient memory, etc.), the system logs a warning and automatically falls back to CPU mode. No manual configuration is required.
| Mode | Backend | Use it for |
|---|---|---|
auto (default) | Platform-specific GPU priority | Automatic GPU detection with CPU fallback |
cpu | ONNX Runtime CPU | Portable, widest compatibility |
coreml | Native CoreML | macOS with CoreML acceleration |
coreml-fast | Native CoreML (2s step) | macOS, faster on long meetings |
cuda | ONNX Runtime CUDA | NVIDIA GPU |
cuda-fast | ONNX Runtime CUDA (2s step) | NVIDIA GPU, faster on long meetings |
migraphx | ONNX Runtime MIGraphX | AMD GPU |
To override automatic detection and force a specific mode:
meeting-agent config set diarize.execution_mode cpu
With the default online feature, speakrs downloads models on first use
from avencera/speakrs-models
to a local cache. Set DIARIZE_MODEL_DIR to point at a pre-bundled model
directory for offline/airgapped setups:
meeting-agent config set diarize.model_dir /opt/speakrs-models
| Variable | Default | Description |
|---|---|---|
DIARIZE_ENABLED | false | Enable speaker diarization during import |
DIARIZE_EXECUTION_MODE | auto | auto | cpu | coreml | coreml-fast | cuda | cuda-fast | migraphx |
DIARIZE_MODEL_DIR | (blank) | Local model dir; blank = download on first use |
ffmpeg not foundAudio conversion and chunking require ffmpeg + ffprobe on your PATH:
# macOS
brew install ffmpeg
# Ubuntu/Debian
sudo apt install ffmpeg
Check TRANSCRIPTION_API_KEY is set and valid:
meeting-agent config show
# Verify api_key field is not "(not set)"
Long audio is auto-chunked. Adjust chunk settings:
meeting-agent config set transcription.chunk_seconds 300
meeting-agent config set transcription.chunk_concurrency 4
Ensure diarize.enabled is true and the execution mode is valid for
your platform:
meeting-agent config set diarize.enabled true
meeting-agent config show
The first import with diarization enabled downloads the speakrs models
(~hundreds of MB) on first use; subsequent imports reuse the cached
pipeline. For offline setups, point diarize.model_dir at a pre-bundled
model directory.
The config file (~/.meeting-agent/config.json) is created with chmod 600
(owner read/write only). If permissions are wrong:
chmod 600 ~/.meeting-agent/config.json
Delete the config file and run any command — a fresh default config is auto-created:
rm ~/.meeting-agent/config.json
meeting-agent config show
Create /etc/systemd/system/meeting-agent.service:
[Unit]
Description=Meeting Agent API Server
After=network.target
[Service]
Type=simple
User=meeting
WorkingDirectory=/opt/meeting-agent
EnvironmentFile=/opt/meeting-agent/.env
ExecStart=/opt/meeting-agent/meeting-agent-server
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now meeting-agent
FROM rust:1.70-slim as builder
WORKDIR /app
COPY . .
RUN apt-get update && apt-get install -y ffmpeg && cargo build --release
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ffmpeg && rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/target/release/meeting-agent-server /usr/local/bin/
EXPOSE 8080
CMD ["meeting-agent-server"]
docker build -t meeting-agent .
docker run -p 8080:8080 -v ~/.meeting-agent:/root/.meeting-agent meeting-agent
server {
listen 80;
server_name meetings.example.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# SSE support (for /jobs/{id}/events)
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 86400;
}
}
MIT
BMW ECE NTUST
200 commits
Rust
91.5%
TypeScript
8.0%