Build a 2026 AI Meeting Minutes Generator with Next.js, Bun.js, and Whisper.cpp: A Full-Stack Developer Tutorial
Learn to build a privacy-first meeting transcription and summarization app using Next.js, Bun.js, and Whisper.cpp in this hands-on 2026 tutorial.
Agentic Academy Labs
Author
In 2026, the modern software team is drowning in synchronous calls, async voice messages, and hybrid stand-ups. Despite advances in collaboration tooling, the humble meeting minutes document remains stuck in the past: a human frantically typing, or a bot that sends a raw transcript nobody reads. This tutorial flips the script by showing you how to build a production-ready AI Meeting Minutes Generator using three cutting-edge technologies: Next.js for the frontend and API layer, Bun.js as the high-performance runtime, and Whisper.cpp for on-device speech-to-text.
By the end, you will have a single codebase that accepts an audio file, transcribes it locally with C++ speed, and calls an LLM to emit clean, structured markdown minutes. Let's dive in.
Why This Stack?
The JavaScript ecosystem in 2026 is faster than ever, but choosing the right foundation matters.
- Next.js (App Router): Provides file-based routing, serverless API routes, and React Server Components. It lets us host the UI and the transcription endpoint in one deployable unit.
- Bun.js: A drop-in Node alternative with a built-in bundler, test runner, and package manager. Benchmarks show 3x faster package installs and 2x lower memory footprint compared to Node 20.
- Whisper.cpp: A port of OpenAI's Whisper to pure C++ with optional SIMD acceleration. It runs on a laptop CPU in real time, meaning no per-minute cloud fees.
Together they form a privacy-first, cost-efficient pipeline that scales from a side project to an enterprise compliance tool.
Prerequisites
Before writing code, ensure your environment matches these requirements:
- Familiarity with TypeScript and React hooks
- Bun v1.3 or later installed globally (
bun --version) - A C++ compiler toolchain (clang or gcc) for building Whisper.cpp
- Downloaded Whisper base model (
ggml-base.en.bin) from the official Hugging Face repo - An LLM API key (OpenAI, Anthropic, or a local Llama.cpp server) for summarization
Project Setup with Bun
Initialize the project using Bun's create command, which is noticeably quicker than npm or yarn:
bun create next@latest meeting-minutes --ts
cd meeting-minutes
bun add whispercpp-node dotenv
bun add -d @types/bun
The whispercpp-node package wraps the native binary and exposes a promise-based API. We also add dotenv to load secrets locally.
Integrating Whisper.cpp
Whisper.cpp requires a model file. Place it in models/ at the project root. Then create lib/transcribe.ts:
import { Whisper } from 'whispercpp-node';
import path from 'node:path';
const modelPath = path.join(process.cwd(), 'models', 'ggml-base.en.bin');
const whisper = new Whisper({ modelPath, threadCount: 4 });
export async function transcribe(audioPath: string): Promise<string> {
const result = await whisper.transcribe(audioPath, {
language: 'en',
translate: false,
});
return result.text.trim();
}
This function spins up a worker pool and returns the decoded text. For longer meetings, you can stream partial results via callbacks.
Building the Next.js Frontend
Our UI needs an upload widget and a preview pane. Edit app/page.tsx:
'use client';
import { useState } from 'react';
export default function Home() {
const [file, setFile] = useState<File | null>(null);
const [minutes, setMinutes] = useState<string>('');
const [loading, setLoading] = useState(false);
async function handleUpload() {
if (!file) return;
setLoading(true);
const fd = new FormData();
fd.append('audio', file);
const res = await fetch('/api/minutes', { method: 'POST', body: fd });
const data = await res.json();
setMinutes(data.minutes);
setLoading(false);
}
return (
<main style={{ maxWidth: 800, margin: '0 auto' }}>
<h1>🎙️ AI Meeting Minutes</h1>
<input type="audio/*" onChange={(e) => setFile(e.target.files?.[0] ?? null)} />
<button onClick={handleUpload} disabled={loading}>
{loading ? 'Generating…' : 'Generate Minutes'}
</button>
<section>
<h2>Output</h2>
<pre>{minutes}</pre>
</section>
</main>
);
}
The component manages local state and posts the file to our API. The accept attribute restricts uploads to audio files.
Creating the API Route
Next.js App Router makes it easy to accept multipart form data. Create app/api/minutes/route.ts:
import { NextRequest } from 'next/server';
import { writeFile } from 'fs/promises';
import { transcribe } from '@/lib/transcribe';
import { generateMinutes } from '@/lib/llm';
export async function POST(req: NextRequest) {
const form = await req.formData();
const audio = form.get('audio') as File;
if (!audio) return new Response('No audio', { status: 400 });
const bytes = Buffer.from(await audio.arrayBuffer());
const tmpPath = `/tmp/${Date.now()}.wav`;
await writeFile(tmpPath, bytes);
const transcript = await transcribe(tmpPath);
const minutes = await generateMinutes(transcript);
return Response.json({ minutes });
}
Generating Meeting Minutes with an LLM
Raw transcripts are noisy. We use a structured prompt to extract decisions and action items. In lib/llm.ts:
export async function generateMinutes(transcript: string): Promise<string> {
const endpoint = process.env.LLM_ENDPOINT ?? 'https://api.openai.com/v1/chat/completions';
const key = process.env.LLM_API_KEY!;
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${key}` },
body: JSON.stringify({
model: process.env.LLM_MODEL ?? 'gpt-5-mini',
messages: [
{ role: 'system', content: 'You are a meticulous executive assistant. Convert the meeting transcript into markdown minutes with sections: ## Attendees, ## Decisions, ## Action Items, ## Follow-ups.' },
{ role: 'user', content: transcript },
],
temperature: 0.2,
}),
});
const data = await res.json();
return data.choices[0].message.content;
}
Running and Testing
Launch the dev server under Bun to benefit from fast refresh:
bun run dev
Navigate to http://localhost:3000, upload a sample recording (try a 2-minute team sync), and observe the terminal logs as Whisper.cpp processes the audio. Within seconds, the right pane fills with formatted minutes.
Troubleshooting Common Issues
- Model not found: Verify the path in
lib/transcribe.tsmatches your file location. - Bun permission errors: Run
bun install --unsafe-permif native bindings fail to compile. - LLM rate limits: Implement retry with exponential backoff in
lib/llm.ts.
Production Tips for 2026
- Edge transcription: Compile Whisper.cpp to WebAssembly and deploy to Cloudflare Workers for zero-latency global inference.
- Speaker labels: Pair with a diarization model like
pyannoteto prefix each line with a name. - Cost monitoring: Use Bun's built-in metrics to track memory per request and set quotas.
Conclusion
You have just assembled a full-stack AI Meeting Minutes Generator that respects user privacy, runs on a single runtime, and delivers immediate value. As 2026 unfolds, the combination of Next.js, Bun.js, and Whisper.cpp will power countless internal tools. Fork the repo, add real-time streaming, and share your enhancements with the Agentic Academy Labs community.
Enjoyed this article?
Read More Articles