Table of Contents

Generate Structured Meeting Notes from Audio Recordings

Every organization records meetings, but few have the resources to turn hours of audio into structured, searchable notes. LM-Kit.NET lets you chain speech-to-text directly into LLM-powered summarization and formatting, converting raw meeting audio into professional notes with agendas, discussion summaries, decisions, and action items. The entire pipeline runs locally, ensuring that confidential board discussions, strategy sessions, and HR conversations never leave your infrastructure. This tutorial builds an automated meeting notes generator that produces structured Markdown output from audio files.


Why Automated Meeting Notes Matter

Two enterprise problems that automated meeting note generation solves:

  1. Executive and board meetings. C-suite meetings and board sessions produce critical decisions that must be documented for governance, compliance, and institutional memory. Manual note-taking is incomplete (the note-taker misses content while writing) and slow (professional minutes take hours to draft). An automated pipeline captures everything and produces structured minutes within seconds of the meeting ending.
  2. Distributed engineering teams. Global teams across time zones rely on recorded standups, design reviews, and retrospectives. Team members who could not attend need concise, structured notes rather than hour-long recordings. Automated notes with section headers let engineers jump directly to the decisions and action items relevant to their work.

Prerequisites

Requirement Minimum
.NET SDK 8.0+
VRAM ~4.5 GB (Whisper model + chat model)
Disk ~4 GB free for model downloads
Audio file A .wav file (16-bit PCM, any sample rate)

Step 1: Create the Project

dotnet new console -n MeetingNotesGenerator
cd MeetingNotesGenerator
dotnet add package LM-Kit.NET

Step 2: Understand the Pipeline

  Meeting recording (.wav)
        │
        ▼
  ┌──────────────────┐
  │  SpeechToText    │    Whisper model
  │  (transcribe)    │    Full transcript with timestamps
  └────────┬─────────┘
           │
           ▼
  ┌──────────────────┐
  │  Summarizer      │    Generate title + executive summary
  │  (summarize)     │
  └────────┬─────────┘
           │
           ▼
  ┌─────────────────────────────┐
  │  SingleTurnConversation     │    Structure as meeting notes
  │  (format as meeting notes)  │    with headers and sections
  └────────┬────────────────────┘
           │
           ▼
  Structured meeting notes (Markdown)
  ├── Meeting title
  ├── Executive summary
  ├── Discussion topics
  ├── Decisions made
  └── Action items

Step 3: Transcribe and Generate Meeting Notes

using System.Text;
using LMKit.Media.Audio;
using LMKit.Model;
using LMKit.Speech;
using LMKit.TextGeneration;
using LMKit.TextGeneration.Chat;

LMKit.Licensing.LicenseManager.SetLicenseKey("");

Console.InputEncoding = Encoding.UTF8;
Console.OutputEncoding = Encoding.UTF8;

// ──────────────────────────────────────
// 1. Load Whisper model
// ──────────────────────────────────────
Console.WriteLine("Loading Whisper model...");
using LM whisperModel = LM.LoadFromModelID("whisper-large-turbo3",
    downloadingProgress: (_, len, read) =>
    {
        if (len.HasValue) Console.Write($"\r  Downloading: {(double)read / len.Value * 100:F1}%   ");
        return true;
    },
    loadingProgress: p => { Console.Write($"\r  Loading: {p * 100:F0}%   "); return true; });
Console.WriteLine("\n");

// ──────────────────────────────────────
// 2. Load chat model for note generation
// ──────────────────────────────────────
Console.WriteLine("Loading chat model...");
using LM chatModel = LM.LoadFromModelID("qwen3:8b",
    downloadingProgress: (_, len, read) =>
    {
        if (len.HasValue) Console.Write($"\r  Downloading: {(double)read / len.Value * 100:F1}%   ");
        return true;
    },
    loadingProgress: p => { Console.Write($"\r  Loading: {p * 100:F0}%   "); return true; });
Console.WriteLine("\n");

// ──────────────────────────────────────
// 3. Transcribe the meeting
// ──────────────────────────────────────
string audioPath = "meeting.wav";
if (!File.Exists(audioPath))
{
    Console.WriteLine($"Place a WAV file at '{audioPath}' and run again.");
    return;
}

var stt = new SpeechToText(whisperModel)
{
    EnableVoiceActivityDetection = true,
    SuppressNonSpeechTokens = true,
    SuppressHallucinations = true
};

Console.WriteLine($"Transcribing {audioPath}...");
using var audio = new WaveFile(audioPath);
Console.WriteLine($"  Duration: {audio.Duration:mm\\:ss\\.ff}\n");

var transcription = stt.Transcribe(audio);

// Build a timestamped transcript
var timestamped = new StringBuilder();
foreach (var seg in transcription.Segments)
{
    timestamped.AppendLine($"[{seg.Start:mm\\:ss}] {seg.Text}");
}

string transcript = transcription.Text;

Console.ForegroundColor = ConsoleColor.DarkGray;
Console.WriteLine($"Transcribed {transcription.Segments.Count} segments\n");
Console.ResetColor();

// ──────────────────────────────────────
// 4. Generate executive summary
// ──────────────────────────────────────
Console.WriteLine("Generating summary...\n");

var summarizer = new Summarizer(chatModel)
{
    MaxContentWords = 100,
    MaxTitleWords = 10,
    GenerateTitle = true,
    GenerateContent = true,
    Intent = Summarizer.SummarizationIntent.Abstraction,
    Guidance = "Focus on the main topics discussed, key decisions made, " +
               "and outcomes. This is a meeting transcript.",
    OverflowStrategy = Summarizer.OverflowResolutionStrategy.RecursiveSummarize
};

Summarizer.SummarizerResult summary = summarizer.Summarize(transcript);

Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"Title: {summary.Title}");
Console.ResetColor();
Console.WriteLine($"Summary: {summary.Summary}\n");

// ──────────────────────────────────────
// 5. Generate structured meeting notes
// ──────────────────────────────────────
Console.WriteLine("Generating meeting notes...\n");

var notesGenerator = new SingleTurnConversation(chatModel)
{
    SystemPrompt = "You are a professional meeting notes writer. Convert the provided meeting " +
                   "transcript into well-structured Markdown meeting notes. Use this format:\n\n" +
                   "# Meeting Notes: [Meeting Title]\n\n" +
                   "**Date:** [if mentioned]\n" +
                   "**Attendees:** [names mentioned in transcript]\n\n" +
                   "## Executive Summary\n" +
                   "[2-3 sentence overview]\n\n" +
                   "## Discussion Topics\n" +
                   "### [Topic 1 Title]\n" +
                   "[Summary of discussion]\n\n" +
                   "### [Topic 2 Title]\n" +
                   "[Summary of discussion]\n\n" +
                   "## Decisions Made\n" +
                   "- [Decision 1]\n" +
                   "- [Decision 2]\n\n" +
                   "## Action Items\n" +
                   "- [ ] [Task] (Owner: [name], Due: [date])\n\n" +
                   "## Next Steps\n" +
                   "[Any follow-up meetings or milestones mentioned]\n\n" +
                   "Rules:\n" +
                   "1. Only include information present in the transcript.\n" +
                   "2. Do not invent attendees, dates, or details.\n" +
                   "3. If information is not available, omit the section.\n" +
                   "4. Use professional, concise language.\n" +
                   "5. Output only the Markdown notes.",
    MaximumCompletionTokens = 4096
};

var notes = new StringBuilder();

notesGenerator.AfterTextCompletion += (_, e) =>
{
    if (e.SegmentType == TextSegmentType.UserVisible)
    {
        notes.Append(e.Text);
        Console.Write(e.Text);
    }
};

notesGenerator.Submit($"Generate meeting notes from this transcript:\n\n{transcript}");
Console.WriteLine("\n");

// Save outputs
string outputPath = Path.ChangeExtension(audioPath, ".notes.md");
File.WriteAllText(outputPath, notes.ToString());
File.WriteAllText(Path.ChangeExtension(audioPath, ".transcript.txt"), transcript);
File.WriteAllText(Path.ChangeExtension(audioPath, ".timestamped.txt"), timestamped.ToString());

Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine($"Saved meeting notes: {outputPath}");
Console.WriteLine($"Saved transcript: {Path.ChangeExtension(audioPath, ".transcript.txt")}");
Console.WriteLine($"Saved timestamped: {Path.ChangeExtension(audioPath, ".timestamped.txt")}");
Console.ResetColor();

Step 4: Domain-Specific Meeting Templates

Customize the note format for different meeting types:

using System.Text;
using LMKit.Media.Audio;
using LMKit.Model;
using LMKit.Speech;
using LMKit.TextGeneration;
using LMKit.TextGeneration.Chat;

LMKit.Licensing.LicenseManager.SetLicenseKey("");

Console.InputEncoding = Encoding.UTF8;
Console.OutputEncoding = Encoding.UTF8;

// ──────────────────────────────────────
// 1. Load Whisper model
// ──────────────────────────────────────
Console.WriteLine("Loading Whisper model...");
using LM whisperModel = LM.LoadFromModelID("whisper-large-turbo3",
    downloadingProgress: (_, len, read) =>
    {
        if (len.HasValue) Console.Write($"\r  Downloading: {(double)read / len.Value * 100:F1}%   ");
        return true;
    },
    loadingProgress: p => { Console.Write($"\r  Loading: {p * 100:F0}%   "); return true; });
Console.WriteLine("\n");

// ──────────────────────────────────────
// 2. Load chat model for note generation
// ──────────────────────────────────────
Console.WriteLine("Loading chat model...");
using LM chatModel = LM.LoadFromModelID("qwen3:8b",
    downloadingProgress: (_, len, read) =>
    {
        if (len.HasValue) Console.Write($"\r  Downloading: {(double)read / len.Value * 100:F1}%   ");
        return true;
    },
    loadingProgress: p => { Console.Write($"\r  Loading: {p * 100:F0}%   "); return true; });
Console.WriteLine("\n");

// ──────────────────────────────────────
// 3. Transcribe the meeting
// ──────────────────────────────────────
string audioPath = "meeting.wav";
if (!File.Exists(audioPath))
{
    Console.WriteLine($"Place a WAV file at '{audioPath}' and run again.");
    return;
}

var stt = new SpeechToText(whisperModel)
{
    EnableVoiceActivityDetection = true,
    SuppressNonSpeechTokens = true,
    SuppressHallucinations = true
};

Console.WriteLine($"Transcribing {audioPath}...");
using var audio = new WaveFile(audioPath);
Console.WriteLine($"  Duration: {audio.Duration:mm\\:ss\\.ff}\n");

var transcription = stt.Transcribe(audio);

// Build a timestamped transcript
var timestamped = new StringBuilder();
foreach (var seg in transcription.Segments)
{
    timestamped.AppendLine($"[{seg.Start:mm\\:ss}] {seg.Text}");
}

string transcript = transcription.Text;

Console.ForegroundColor = ConsoleColor.DarkGray;
Console.WriteLine($"Transcribed {transcription.Segments.Count} segments\n");
Console.ResetColor();

// ──────────────────────────────────────
// 4. Generate executive summary
// ──────────────────────────────────────
Console.WriteLine("Generating summary...\n");

var summarizer = new Summarizer(chatModel)
{
    MaxContentWords = 100,
    MaxTitleWords = 10,
    GenerateTitle = true,
    GenerateContent = true,
    Intent = Summarizer.SummarizationIntent.Abstraction,
    Guidance = "Focus on the main topics discussed, key decisions made, " +
               "and outcomes. This is a meeting transcript.",
    OverflowStrategy = Summarizer.OverflowResolutionStrategy.RecursiveSummarize
};

Summarizer.SummarizerResult summary = summarizer.Summarize(transcript);

Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"Title: {summary.Title}");
Console.ResetColor();
Console.WriteLine($"Summary: {summary.Summary}\n");

// ──────────────────────────────────────
// 5. Generate structured meeting notes
// ──────────────────────────────────────
Console.WriteLine("Generating meeting notes...\n");

var notesGenerator = new SingleTurnConversation(chatModel)
{
    SystemPrompt = "You are a professional meeting notes writer. Convert the provided meeting " +
                   "transcript into well-structured Markdown meeting notes. Use this format:\n\n" +
                   "# Meeting Notes: [Meeting Title]\n\n" +
                   "**Date:** [if mentioned]\n" +
                   "**Attendees:** [names mentioned in transcript]\n\n" +
                   "## Executive Summary\n" +
                   "[2-3 sentence overview]\n\n" +
                   "## Discussion Topics\n" +
                   "### [Topic 1 Title]\n" +
                   "[Summary of discussion]\n\n" +
                   "### [Topic 2 Title]\n" +
                   "[Summary of discussion]\n\n" +
                   "## Decisions Made\n" +
                   "- [Decision 1]\n" +
                   "- [Decision 2]\n\n" +
                   "## Action Items\n" +
                   "- [ ] [Task] (Owner: [name], Due: [date])\n\n" +
                   "## Next Steps\n" +
                   "[Any follow-up meetings or milestones mentioned]\n\n" +
                   "Rules:\n" +
                   "1. Only include information present in the transcript.\n" +
                   "2. Do not invent attendees, dates, or details.\n" +
                   "3. If information is not available, omit the section.\n" +
                   "4. Use professional, concise language.\n" +
                   "5. Output only the Markdown notes.",
    MaximumCompletionTokens = 4096
};

var notes = new StringBuilder();

notesGenerator.AfterTextCompletion += (_, e) =>
{
    if (e.SegmentType == TextSegmentType.UserVisible)
    {
        notes.Append(e.Text);
        Console.Write(e.Text);
    }
};

notesGenerator.Submit($"Generate meeting notes from this transcript:\n\n{transcript}");
Console.WriteLine("\n");

// Sprint retrospective format
var retroGenerator = new SingleTurnConversation(chatModel)
{
    SystemPrompt = "Convert this meeting transcript into sprint retrospective notes in Markdown:\n\n" +
                   "# Sprint Retrospective: [Sprint Name/Number]\n\n" +
                   "## What Went Well\n- [items]\n\n" +
                   "## What Needs Improvement\n- [items]\n\n" +
                   "## Action Items for Next Sprint\n- [ ] [task] (Owner: [name])\n\n" +
                   "Only include information from the transcript. Use concise language.",
    MaximumCompletionTokens = 4096
};

// Board meeting minutes format
var boardGenerator = new SingleTurnConversation(chatModel)
{
    SystemPrompt = "Convert this transcript into formal board meeting minutes in Markdown:\n\n" +
                   "# Board Meeting Minutes\n\n" +
                   "**Date:** **Location:** **Present:** **Absent:**\n\n" +
                   "## Call to Order\n\n" +
                   "## Approval of Previous Minutes\n\n" +
                   "## Reports\n### [Report Title]\n\n" +
                   "## Old Business\n\n## New Business\n\n" +
                   "## Motions and Votes\n" +
                   "| Motion | Moved By | Seconded | Result |\n|---|---|---|---|\n\n" +
                   "## Adjournment\n\n" +
                   "Use formal language. Only include information from the transcript.",
    MaximumCompletionTokens = 4096
};

// One-on-one meeting notes
var oneOnOneGenerator = new SingleTurnConversation(chatModel)
{
    SystemPrompt = "Convert this transcript into 1:1 meeting notes in Markdown:\n\n" +
                   "# 1:1 Meeting Notes\n\n" +
                   "## Updates Since Last Meeting\n- [items]\n\n" +
                   "## Discussion Points\n- [items]\n\n" +
                   "## Feedback Given\n- [items]\n\n" +
                   "## Goals and Priorities\n- [items]\n\n" +
                   "## Action Items\n- [ ] [task] (Owner: [name])\n\n" +
                   "Keep notes concise. Only include information from the transcript.",
    MaximumCompletionTokens = 4096
};

Step 5: Include Timestamped References

Generate notes that link back to specific moments in the recording:

using System.Text;
using LMKit.Media.Audio;
using LMKit.Model;
using LMKit.Speech;
using LMKit.TextGeneration;
using LMKit.TextGeneration.Chat;

LMKit.Licensing.LicenseManager.SetLicenseKey("");

Console.InputEncoding = Encoding.UTF8;
Console.OutputEncoding = Encoding.UTF8;

// ──────────────────────────────────────
// 1. Load Whisper model
// ──────────────────────────────────────
Console.WriteLine("Loading Whisper model...");
using LM whisperModel = LM.LoadFromModelID("whisper-large-turbo3",
    downloadingProgress: (_, len, read) =>
    {
        if (len.HasValue) Console.Write($"\r  Downloading: {(double)read / len.Value * 100:F1}%   ");
        return true;
    },
    loadingProgress: p => { Console.Write($"\r  Loading: {p * 100:F0}%   "); return true; });
Console.WriteLine("\n");

// ──────────────────────────────────────
// 2. Load chat model for note generation
// ──────────────────────────────────────
Console.WriteLine("Loading chat model...");
using LM chatModel = LM.LoadFromModelID("qwen3:8b",
    downloadingProgress: (_, len, read) =>
    {
        if (len.HasValue) Console.Write($"\r  Downloading: {(double)read / len.Value * 100:F1}%   ");
        return true;
    },
    loadingProgress: p => { Console.Write($"\r  Loading: {p * 100:F0}%   "); return true; });
Console.WriteLine("\n");

// ──────────────────────────────────────
// 3. Transcribe the meeting
// ──────────────────────────────────────
string audioPath = "meeting.wav";
if (!File.Exists(audioPath))
{
    Console.WriteLine($"Place a WAV file at '{audioPath}' and run again.");
    return;
}

var stt = new SpeechToText(whisperModel)
{
    EnableVoiceActivityDetection = true,
    SuppressNonSpeechTokens = true,
    SuppressHallucinations = true
};

Console.WriteLine($"Transcribing {audioPath}...");
using var audio = new WaveFile(audioPath);
Console.WriteLine($"  Duration: {audio.Duration:mm\\:ss\\.ff}\n");

var transcription = stt.Transcribe(audio);

// Build a timestamped transcript
var timestamped = new StringBuilder();
foreach (var seg in transcription.Segments)
{
    timestamped.AppendLine($"[{seg.Start:mm\\:ss}] {seg.Text}");
}

string transcript = transcription.Text;

Console.ForegroundColor = ConsoleColor.DarkGray;
Console.WriteLine($"Transcribed {transcription.Segments.Count} segments\n");
Console.ResetColor();

// ──────────────────────────────────────
// 4. Generate executive summary
// ──────────────────────────────────────
Console.WriteLine("Generating summary...\n");

var summarizer = new Summarizer(chatModel)
{
    MaxContentWords = 100,
    MaxTitleWords = 10,
    GenerateTitle = true,
    GenerateContent = true,
    Intent = Summarizer.SummarizationIntent.Abstraction,
    Guidance = "Focus on the main topics discussed, key decisions made, " +
               "and outcomes. This is a meeting transcript.",
    OverflowStrategy = Summarizer.OverflowResolutionStrategy.RecursiveSummarize
};

Summarizer.SummarizerResult summary = summarizer.Summarize(transcript);

Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"Title: {summary.Title}");
Console.ResetColor();
Console.WriteLine($"Summary: {summary.Summary}\n");

// ──────────────────────────────────────
// 5. Generate structured meeting notes
// ──────────────────────────────────────
Console.WriteLine("Generating meeting notes...\n");

var notesGenerator = new SingleTurnConversation(chatModel)
{
    SystemPrompt = "You are a professional meeting notes writer. Convert the provided meeting " +
                   "transcript into well-structured Markdown meeting notes. Use this format:\n\n" +
                   "# Meeting Notes: [Meeting Title]\n\n" +
                   "**Date:** [if mentioned]\n" +
                   "**Attendees:** [names mentioned in transcript]\n\n" +
                   "## Executive Summary\n" +
                   "[2-3 sentence overview]\n\n" +
                   "## Discussion Topics\n" +
                   "### [Topic 1 Title]\n" +
                   "[Summary of discussion]\n\n" +
                   "### [Topic 2 Title]\n" +
                   "[Summary of discussion]\n\n" +
                   "## Decisions Made\n" +
                   "- [Decision 1]\n" +
                   "- [Decision 2]\n\n" +
                   "## Action Items\n" +
                   "- [ ] [Task] (Owner: [name], Due: [date])\n\n" +
                   "## Next Steps\n" +
                   "[Any follow-up meetings or milestones mentioned]\n\n" +
                   "Rules:\n" +
                   "1. Only include information present in the transcript.\n" +
                   "2. Do not invent attendees, dates, or details.\n" +
                   "3. If information is not available, omit the section.\n" +
                   "4. Use professional, concise language.\n" +
                   "5. Output only the Markdown notes.",
    MaximumCompletionTokens = 4096
};

var notes = new StringBuilder();

notesGenerator.AfterTextCompletion += (_, e) =>
{
    if (e.SegmentType == TextSegmentType.UserVisible)
    {
        notes.Append(e.Text);
        Console.Write(e.Text);
    }
};

notesGenerator.Submit($"Generate meeting notes from this transcript:\n\n{transcript}");
Console.WriteLine("\n");

Console.WriteLine("\n=== Meeting Notes with Timestamps ===\n");

var timestampedGenerator = new SingleTurnConversation(chatModel)
{
    SystemPrompt = "Convert this timestamped meeting transcript into structured meeting notes " +
                   "in Markdown. For each major discussion topic and decision, include the " +
                   "timestamp reference in parentheses (e.g., '(at 05:23)'). This helps " +
                   "readers jump to the relevant part of the recording.\n\n" +
                   "Format:\n" +
                   "# Meeting Notes\n\n" +
                   "## Discussion Topics\n" +
                   "### [Topic] (at MM:SS)\n[Summary]\n\n" +
                   "## Decisions\n- [Decision] (at MM:SS)\n\n" +
                   "## Action Items\n- [ ] [Task] (Owner, Due date)\n\n" +
                   "Only include information from the transcript.",
    MaximumCompletionTokens = 4096
};

var tsNotes = new StringBuilder();

timestampedGenerator.AfterTextCompletion += (_, e) =>
{
    if (e.SegmentType == TextSegmentType.UserVisible)
    {
        tsNotes.Append(e.Text);
        Console.Write(e.Text);
    }
};

// Use the timestamped transcript (with [MM:SS] prefixes)
timestampedGenerator.Submit(
    $"Generate timestamped meeting notes from this transcript:\n\n{timestamped}");
Console.WriteLine("\n");

File.WriteAllText("meeting_notes_timestamped.md", tsNotes.ToString());

Step 6: Batch Process a Meeting Archive

Process multiple recordings and generate an index:

using System.Text;
using LMKit.Media.Audio;
using LMKit.Model;
using LMKit.Speech;
using LMKit.TextGeneration;
using LMKit.TextGeneration.Chat;

LMKit.Licensing.LicenseManager.SetLicenseKey("");

Console.InputEncoding = Encoding.UTF8;
Console.OutputEncoding = Encoding.UTF8;

// ──────────────────────────────────────
// 1. Load Whisper model
// ──────────────────────────────────────
Console.WriteLine("Loading Whisper model...");
using LM whisperModel = LM.LoadFromModelID("whisper-large-turbo3",
    downloadingProgress: (_, len, read) =>
    {
        if (len.HasValue) Console.Write($"\r  Downloading: {(double)read / len.Value * 100:F1}%   ");
        return true;
    },
    loadingProgress: p => { Console.Write($"\r  Loading: {p * 100:F0}%   "); return true; });
Console.WriteLine("\n");

// ──────────────────────────────────────
// 2. Load chat model for note generation
// ──────────────────────────────────────
Console.WriteLine("Loading chat model...");
using LM chatModel = LM.LoadFromModelID("qwen3:8b",
    downloadingProgress: (_, len, read) =>
    {
        if (len.HasValue) Console.Write($"\r  Downloading: {(double)read / len.Value * 100:F1}%   ");
        return true;
    },
    loadingProgress: p => { Console.Write($"\r  Loading: {p * 100:F0}%   "); return true; });
Console.WriteLine("\n");

// ──────────────────────────────────────
// 3. Transcribe the meeting
// ──────────────────────────────────────
string audioPath = "meeting.wav";
if (!File.Exists(audioPath))
{
    Console.WriteLine($"Place a WAV file at '{audioPath}' and run again.");
    return;
}

var stt = new SpeechToText(whisperModel)
{
    EnableVoiceActivityDetection = true,
    SuppressNonSpeechTokens = true,
    SuppressHallucinations = true
};

Console.WriteLine($"Transcribing {audioPath}...");
using var audio = new WaveFile(audioPath);
Console.WriteLine($"  Duration: {audio.Duration:mm\\:ss\\.ff}\n");

var transcription = stt.Transcribe(audio);

// Build a timestamped transcript
var timestamped = new StringBuilder();
foreach (var seg in transcription.Segments)
{
    timestamped.AppendLine($"[{seg.Start:mm\\:ss}] {seg.Text}");
}

string transcript = transcription.Text;

Console.ForegroundColor = ConsoleColor.DarkGray;
Console.WriteLine($"Transcribed {transcription.Segments.Count} segments\n");
Console.ResetColor();

// ──────────────────────────────────────
// 4. Generate executive summary
// ──────────────────────────────────────
Console.WriteLine("Generating summary...\n");

var summarizer = new Summarizer(chatModel)
{
    MaxContentWords = 100,
    MaxTitleWords = 10,
    GenerateTitle = true,
    GenerateContent = true,
    Intent = Summarizer.SummarizationIntent.Abstraction,
    Guidance = "Focus on the main topics discussed, key decisions made, " +
               "and outcomes. This is a meeting transcript.",
    OverflowStrategy = Summarizer.OverflowResolutionStrategy.RecursiveSummarize
};

Summarizer.SummarizerResult summary = summarizer.Summarize(transcript);

Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"Title: {summary.Title}");
Console.ResetColor();
Console.WriteLine($"Summary: {summary.Summary}\n");

// ──────────────────────────────────────
// 5. Generate structured meeting notes
// ──────────────────────────────────────
Console.WriteLine("Generating meeting notes...\n");

var notesGenerator = new SingleTurnConversation(chatModel)
{
    SystemPrompt = "You are a professional meeting notes writer. Convert the provided meeting " +
                   "transcript into well-structured Markdown meeting notes. Use this format:\n\n" +
                   "# Meeting Notes: [Meeting Title]\n\n" +
                   "**Date:** [if mentioned]\n" +
                   "**Attendees:** [names mentioned in transcript]\n\n" +
                   "## Executive Summary\n" +
                   "[2-3 sentence overview]\n\n" +
                   "## Discussion Topics\n" +
                   "### [Topic 1 Title]\n" +
                   "[Summary of discussion]\n\n" +
                   "### [Topic 2 Title]\n" +
                   "[Summary of discussion]\n\n" +
                   "## Decisions Made\n" +
                   "- [Decision 1]\n" +
                   "- [Decision 2]\n\n" +
                   "## Action Items\n" +
                   "- [ ] [Task] (Owner: [name], Due: [date])\n\n" +
                   "## Next Steps\n" +
                   "[Any follow-up meetings or milestones mentioned]\n\n" +
                   "Rules:\n" +
                   "1. Only include information present in the transcript.\n" +
                   "2. Do not invent attendees, dates, or details.\n" +
                   "3. If information is not available, omit the section.\n" +
                   "4. Use professional, concise language.\n" +
                   "5. Output only the Markdown notes.",
    MaximumCompletionTokens = 4096
};

var notes = new StringBuilder();

notesGenerator.AfterTextCompletion += (_, e) =>
{
    if (e.SegmentType == TextSegmentType.UserVisible)
    {
        notes.Append(e.Text);
        Console.Write(e.Text);
    }
};

notesGenerator.Submit($"Generate meeting notes from this transcript:\n\n{transcript}");
Console.WriteLine("\n");

Console.WriteLine("\n=== Batch Meeting Notes ===\n");

string meetingsDir = "meetings";
string outputDir = "meeting_notes";

if (!Directory.Exists(meetingsDir))
{
    Console.WriteLine($"Create a '{meetingsDir}' folder with WAV files, then run again.");
    return;
}

Directory.CreateDirectory(outputDir);

string[] wavFiles = Directory.GetFiles(meetingsDir, "*.wav");
Console.WriteLine($"Found {wavFiles.Length} recording(s)\n");

var indexLines = new List<string>();
indexLines.Add("# Meeting Notes Index\n");
indexLines.Add("| File | Title | Summary |");
indexLines.Add("|---|---|---|");

foreach (string wavPath in wavFiles)
{
    string meetingName = Path.GetFileNameWithoutExtension(wavPath);
    Console.Write($"  {Path.GetFileName(wavPath)}: ");

    try
    {
        // Transcribe
        using var wav = new WaveFile(wavPath);
        var meetingResult = stt.Transcribe(wav);

        // Summarize for the index
        Summarizer.SummarizerResult meetingSummary = summarizer.Summarize(meetingResult.Text);

        // Generate full notes
        var meetingNotes = new StringBuilder();
        notesGenerator.AfterTextCompletion += (_, e) =>
        {
            if (e.SegmentType == TextSegmentType.UserVisible)
                meetingNotes.Append(e.Text);
        };

        notesGenerator.Submit(
            $"Generate meeting notes from this transcript:\n\n{meetingResult.Text}");

        // Save notes
        string notesPath = Path.Combine(outputDir, $"{meetingName}.md");
        File.WriteAllText(notesPath, meetingNotes.ToString());

        // Add to index
        string title = (meetingSummary.Title ?? meetingName).Replace("|", "\\|");
        string content = (meetingSummary.Summary ?? "").Replace("|", "\\|").Replace("\n", " ");
        if (content.Length > 120) content = content[..120] + "...";
        indexLines.Add($"| [{meetingName}]({meetingName}.md) | {title} | {content} |");

        Console.ForegroundColor = ConsoleColor.Green;
        Console.WriteLine($"done → {notesPath}");
        Console.ResetColor();
    }
    catch (Exception ex)
    {
        Console.ForegroundColor = ConsoleColor.Red;
        Console.WriteLine($"failed: {ex.Message}");
        Console.ResetColor();
    }
}

// Save index
string indexPath = Path.Combine(outputDir, "INDEX.md");
File.WriteAllLines(indexPath, indexLines);
Console.WriteLine($"\nIndex saved: {indexPath}");
Console.WriteLine($"All notes saved to {Path.GetFullPath(outputDir)}");

Model Selection

Whisper Models (Transcription)

Model ID VRAM Speed Best For
whisper-large-turbo3 ~870 MB Moderate Best accuracy (recommended)
whisper-small ~260 MB Fast Large meeting backlogs

Chat Models (Note Generation)

Model ID VRAM Quality Best For
gemma3:4b ~3.5 GB Good Short meetings, simple agendas
qwen3:8b ~6 GB Very good Complex meetings, multiple topics (recommended)
gemma3:12b ~8 GB Excellent Dense board meetings, formal minutes

Common Issues

Problem Cause Fix
Notes miss topics from second half Transcript too long for context window Use RecursiveSummarize overflow strategy; split transcript into chunks
Invented attendee names LLM hallucinated names not in transcript Strengthen the system prompt: "Only include names explicitly mentioned"
Notes too brief MaximumCompletionTokens too low Increase to 4096 or higher
Wrong meeting type template Used generic format for specialized meeting Use domain-specific templates (Step 4)
Timestamps missing from notes Used plain transcript instead of timestamped version Pass the timestamped transcript (Step 5)

Next Steps

Share