Upload a recording, wait for processing, review the summary, then receive or share the minutes.
Executive summary
What we built
The tool accepts a meeting recording or transcript, produces structured meeting minutes, and can send the result by email. The important design choice is privacy: try local processing first, then use cloud processing only as a fallback path.
Colleagues do not need to understand the backend. They just need a browser and, if enabled, access permission.
Meeting audio is sensitive, so local transcription is preferred before cloud fallback is considered.
Build journey
From script to usable workflow
The project grew in small, practical steps. Each step removed friction for the next type of user.
Local audio transcriber
We started with a simple Python-based local transcriber. This was useful as a private tool, but it still required technical comfort and manual file handling.
Meeting-minutes generator
The raw transcript was turned into structured outputs: summary, decisions, action items, unresolved questions, full transcript, and browser-friendly HTML minutes.
Telegram agent intake
Telegram made the tool easier to use. Instead of manually moving files around, a user could upload audio to an agent and let the workflow handle the backend routing.
Online frontend
A browser interface made the system easier for colleagues. It supports file upload, local-first processing, visible status, generated minutes, and optional email delivery.
Cloud fallback and email delivery
Cloud fallback improves availability when the local processor is offline or unavailable. Email delivery then completes the workflow by sending the generated result to the user.
Original foundation
The minutes-taker core we built upon
The first serious version was not a web app. It was a local Python pipeline that took one file, created a meeting ID, extracted or accepted a transcript, checked quality, generated structured minutes, and wrote the outputs to disk.
src/meeting_agent/cli.py is the core processor. It handles the file, local audio transcription, transcript quality report, summary, action items, HTML output, and fallback request.
server.mjs turned that processor into a local upload API. The browser sends a file in, the server saves it, runs Python, reads the outputs, and optionally emails the result.
def transcribe_audio_local(input_path: Path, output_dir: Path) -> tuple[str, dict]:
if not shutil.which("ffmpeg"):
quality = QualityReport(
ok_for_minutes=False,
needs_review=True,
issues=["ffmpeg is not installed or not on PATH; local audio transcription cannot run."],
detected_languages=[],
speaker_count=0,
word_count=0,
)
write_cloud_fallback_request(input_path, output_dir, quality)
raise SystemExit("Local audio transcription needs ffmpeg.")
try:
from faster_whisper import WhisperModel
except ImportError:
quality = QualityReport(
ok_for_minutes=False,
needs_review=True,
issues=["faster-whisper is not installed; local audio transcription cannot run."],
detected_languages=[],
speaker_count=0,
word_count=0,
)
write_cloud_fallback_request(input_path, output_dir, quality)
raise SystemExit("Install optional dependencies or review cloud fallback.")
model_name = os.environ.get("MEETING_AGENT_WHISPER_MODEL", "small")
language = os.environ.get("MEETING_AGENT_WHISPER_LANGUAGE", "en").strip() or None
model = WhisperModel(model_name, device="auto", compute_type="auto")
segments_iter, info = model.transcribe(str(input_path), vad_filter=True, language=language)
# Each Whisper segment becomes a timestamped transcript line.
# Later steps turn those lines into summary, decisions, and action items.
- Took a local audio or video file.
- Checked whether
ffmpegwas available for media handling. - Loaded
faster-whisperfor local speech-to-text transcription. - Produced timestamped transcript lines from Whisper segments.
- Kept the sensitive recording on the local machine where possible.
- Minutes generation: summary, decisions, action items, risks, and questions.
- The frontend gives colleagues a normal upload screen.
- The local Node server exposes
/api/processand/api/health. - The server calls
python -m meeting_agent.cli process. - The generated files are read back into the browser result view.
- SMTP delivery sends minutes and attachments when an email is provided.
- Telegram can also feed files into the same backend flow as an intake layer.
Mobile access
Turning the script into a Telegram assistant
After the local transcriber worked, the next question was usability: how do we make it available from a phone? The answer was to create a Telegram meeting-minutes assistant called Minnie, so Andy could send audio directly from mobile instead of moving files through folders.
Her soul, identity, tone, and boundaries stayed in her own workspace files. We did not turn her into a generic transcription script.
The meeting-agent path and helper-script flow were added to her workspace instructions so she knew where to send uploaded recordings.
Instead of opening a terminal, Andy could send a voice note or audio file to Minnie and receive back usable meeting notes.
# Meeting-minutes capability
When the user uploads a meeting recording, voice note, or transcript:
1. Treat the file as sensitive meeting material.
2. Route it to the local meeting-agent helper script.
3. Wait for generated outputs: transcript, summary, minutes, decisions, actions.
4. Return the useful result to the user in Telegram.
5. Do not expose tokens, private paths, or raw logs in chat.
Conceptual helper path:
meeting-agent/scripts/process-meeting-file.ps1
-> receives the Telegram-uploaded file
-> calls the local meeting-agent processor
-> reads the generated meeting outputs
-> gives Minnie the summary/minutes to reply with
Architecture
How the pieces connect
The visible feature is small. The backend wiring is the interesting part: intake, routing, local service health checks, fallback processing, output generation, and delivery status.
- Runs on Andy's machine.
- Handles local upload processing.
- Keeps sensitive meeting audio closer to home.
- Can send result email when SMTP is configured.
- Provides a hosted browser entry point.
- Uses protected upload and job status routes.
- Can act as fallback when the local processor is offline.
- Needs access control before wider sharing.
Backend thought process
The full workflow behind the button
The design is a hybrid route. Try the private local path first. If that path is unavailable and policy allows it, use the cloud path. Keep outputs structured, visible, and deliverable.
The transcript can describe what people said, but it cannot change Minnie's role, tools, recipient list, or delivery rules.
Email delivery uses the receiver field from the workflow. The transcript is never allowed to add "everyone", attendees, or other addresses.
Prompt-injection style phrases are marked as review issues before the minutes are shared or emailed.
Best for private meetings. Audio stays on the local machine where possible, and the system generates minutes, decisions, actions, transcript, and HTML output.
Useful when stronger model quality is needed. It should be treated as an approved fallback because transcript text or audio may leave the local machine.
Email sends the generated minutes and attachments to the requested recipient. Raw meeting recordings are not emailed by default.
if summarizer == "openai":
summary_bundle = summarize_with_openai(transcript)
else:
summary_bundle = summarize_heuristic(transcript, segments, quality)
# Plain English:
# - local transcript first where possible
# - local/basic minutes generation by default
# - cloud summary only when configured and approved
# - output still returns as minutes, decisions, actions, and questions
The first question is not "can we transcribe this?" The first question is "where should this recording go?" That is why routing and privacy checks sit before the processing path.
The user sees one upload flow, but the backend chooses between local processing, cloud fallback, generated artifacts, review status, and email delivery.
User guide
How a colleague would use it
This is the non-technical version. The colleague should not need to know what Python, Workers, R2, D1, SMTP, or Markdown are. Good. That is the point.
- Open the meeting-minutes web page.
- Choose the meeting recording, video, or transcript file.
- Enter an email address if they want the result delivered by email.
- Start processing.
- Wait for the status to show whether the local or cloud route is being used.
- Review the generated summary, decisions, action items, and questions.
- Use the emailed result or copy/download the minutes after review.
Privacy design
Why local-first matters
Meeting recordings can include customer information, internal plans, commercial terms, personal details, and accidental side comments. Treating audio as sensitive by default is the sane choice.
Use local transcription when the local processor is online. This is the preferred path for sensitive internal meetings.
Use cloud processing when availability matters and the data is allowed to leave the local machine under company policy.
Access model
Options for colleagues and guests
The right sharing model depends on who needs access and how sensitive the recordings are.
Use login or allowlist access. Company emails are easier to manage than shared passwords.
Use expiring upload links or temporary tokens. Keep the window short and limit what the token can do.
Use local-only mode, disable cloud fallback, and require manual review before sharing minutes externally.
Roadmap
What could come next
The current version is already useful. The next upgrades would improve access control, review flow, and output formats.
- Company email allowlist.
- Cloudflare Access or similar login.
- Temporary guest token flow.
- Approve before emailing.
- Flag uncertain transcript sections.
- Keep a simple processing audit trail.
- PDF export.
- DOCX export.
- Company-branded minutes template.
What looks like "just upload audio and get notes" is actually a complete agent workflow: intake, processing, privacy routing, fallback handling, structured output, and delivery.