### Install Dependencies and Build Project Source: https://context7.com/ericstrohmaier/discord-meeting-transcribe-summary/llms.txt Commands to install project dependencies using pnpm, build the TypeScript code into JavaScript, and start the application. Assumes pnpm is installed and configured. ```bash # Install dependencies with pnpm pnpm install # Build TypeScript to JavaScript pnpm run build # Start the bot pnpm start ``` -------------------------------- ### Environment Variables Setup Source: https://context7.com/ericstrohmaier/discord-meeting-transcribe-summary/llms.txt Configuration for essential environment variables required by the bot, including Discord bot token, client ID, guild ID, and OpenAI API key. These are typically stored in a .env file for security and ease of management. ```bash # .env file configuration TOKEN=your-discord-bot-token CLIENT_ID=your-discord-app-client-id GUILD_ID=your-guild-id OPENAI_API_KEY=your-openai-api-key ``` -------------------------------- ### Development Mode Command Source: https://context7.com/ericstrohmaier/discord-meeting-transcribe-summary/llms.txt Starts the development server with hot reloading enabled using pnpm. This command is intended for local development environments to facilitate rapid iteration on the bot's features. ```shell pnpm run dev ``` -------------------------------- ### Start Meeting Recording with FFmpeg (TypeScript) Source: https://context7.com/ericstrohmaier/discord-meeting-transcribe-summary/llms.txt Initiates a voice channel recording by joining the channel and starting an FFmpeg process to capture and encode audio. It handles real-time audio mixing from multiple users into a single OGG file with Opus codec and manages individual user audio streams. ```typescript import { joinVoiceChannel, EndBehaviorType } from '@discordjs/voice'; import { ChatInputCommandInteraction, GuildMember } from 'discord.js'; import ffmpeg from 'ffmpeg-static'; import { spawn } from 'child_process'; // Usage: /meeting start Weekly Team Sync async function startRecording(interaction: ChatInputCommandInteraction) { const member = interaction.member as GuildMember; const voiceChannel = member.voice.channel; const meetingName = interaction.options.getString('name', true); if (!voiceChannel) { await interaction.reply('You need to be in a voice channel to start a meeting.'); return; } // Join voice channel const connection = joinVoiceChannel({ channelId: voiceChannel.id, guildId: interaction.guild.id, adapterCreator: interaction.guild.voiceAdapterCreator, selfDeaf: false, selfMute: true, }); const meetingDir = `./meetings/${meetingName}`; // Ensure directory exists (implementation omitted for brevity) const oggPath = `${meetingDir}/${meetingName}.ogg`; const recordingProcess = spawn(ffmpeg, [ '-f', 's16le', '-ar', '48000', '-ac', '1', '-i', 'pipe:0', '-c:a', 'libopus', '-b:a', '64k', '-application', 'voip', '-y', oggPath ]); // Mock variables for demonstration const chunkSize = 1024; // Example chunk size const userBuffers = new Map(); // To store audio buffers per user // Mix audio from all users every 20ms const mixingInterval = setInterval(() => { const mixedBuffer = Buffer.alloc(chunkSize); // Combine audio from all user buffers (implementation omitted for brevity) // for (const [userId, user] of userBuffers) { // // Mix user samples into combined buffer // } recordingProcess.stdin.write(mixedBuffer); }, 20); // Subscribe to user audio streams const receiver = connection.receiver; receiver.speaking.on('start', (userId) => { const audioStream = receiver.subscribe(userId, { end: { behavior: EndBehaviorType.AfterSilence, duration: 200 } }); // Decode Opus to PCM and buffer (implementation omitted for brevity) // audioStream.on('data', (chunk) => { // // Process audio chunk // }); }); await interaction.reply(`Recording started for meeting: ${meetingName}`); } // Example of how this might be called within an interaction handler // client.on(Events.InteractionCreate, async (interaction) => { // if (interaction.isChatInputCommand() && interaction.commandName === 'meeting') { // const subCommand = interaction.options.getSubcommand(); // if (subCommand === 'start') { // await startRecording(interaction); // } // } // }); ``` -------------------------------- ### Initialize Discord Bot and Register Commands (TypeScript) Source: https://context7.com/ericstrohmaier/discord-meeting-transcribe-summary/llms.txt Initializes the Discord bot client, loads slash commands from a local directory, and registers them with the Discord API. It also sets up event handlers for interaction creation, including chat input commands and autocomplete. On startup, it scans the 'meetings' directory to restore the state of existing recordings. ```typescript import { Client, GatewayIntentBits, REST, Routes, Events } from 'discord.js'; import dotenv from 'dotenv'; dotenv.config(); const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates], }); // Assume 'commands' is an array of command objects loaded from files const commands = []; // Placeholder for loaded commands // Register slash commands with Discord const rest = new REST().setToken(process.env.TOKEN); await rest.put( Routes.applicationGuildCommands(process.env.CLIENT_ID, process.env.GUILD_ID), { body: commands } ); // Handle command interactions client.on(Events.InteractionCreate, async (interaction) => { if (interaction.isChatInputCommand()) { // Assume client.commands is a Map storing command objects const command = client.commands.get(interaction.commandName); if (command) { await command.execute(interaction); } } }); await client.login(process.env.TOKEN); ``` -------------------------------- ### Summarize Transcription with GPT-4o Source: https://context7.com/ericstrohmaier/discord-meeting-transcribe-summary/llms.txt Generates a summary of a meeting transcription using OpenAI's GPT-4o model. It reads the transcription from a file, crafts a prompt for the AI, and returns the generated summary. Requires an OpenAI API key. ```typescript import fs from 'fs'; import OpenAI from 'openai'; export async function summarize(transcriptionPath: string): Promise { const transcription = fs.readFileSync(transcriptionPath, 'utf-8'); const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); const response = await openai.chat.completions.create({ model: 'gpt-4o', messages: [ { role: 'system', content: 'You are a professional assistant summarizing meeting transcriptions...' }, { role: 'user', content: `Summarize this transcription:\n${transcription}` } ] }); return response.choices[0].message.content || ''; } ``` -------------------------------- ### Default Configuration for OpenAI API Source: https://context7.com/ericstrohmaier/discord-meeting-transcribe-summary/llms.txt Defines default settings for OpenAI API interactions, including model choices for summarization and transcription, language, maximum transcription size, and detailed system and user prompts for summarization tasks. It also specifies allowed roles for bot operation. ```json // config/default.json { "openai": { "summary_model": "gpt-4o", "transcription_model": "whisper-1", "transcription_language": "de", "transcription_max_size_MB": 8, "system_content": [ "You are a professional assistant summarizing meeting transcriptions.", "Create a detailed summary including:", "- Main topics discussed", "- Decisions made", "- Action items and responsibilities", "- Future plans", "- Important information and challenges" ], "user_content": "Summarize this transcription:\n" }, "allowed_roles": ["Admin", "Weekly Transcription Bot Operator"] } ``` -------------------------------- ### Split Audio File using FFmpeg Source: https://context7.com/ericstrohmaier/discord-meeting-transcribe-summary/llms.txt Splits a large audio file into smaller chunks based on a maximum file size, utilizing FFmpeg for the process. This is crucial for adhering to API limits for transcription services. It returns an array of paths to the newly created audio parts. ```typescript import fs from 'fs'; import { spawn } from 'child_process'; import ffmpeg from 'ffmpeg-static'; // Helper function to get audio duration (assumed to be defined elsewhere) async function getAudioDuration(filePath: string): Promise { // Implementation using FFmpeg to get duration return new Promise((resolve, reject) => { const proc = spawn(ffmpeg, ['-i', filePath]); let duration = 0; proc.stderr.on('data', (data) => { const match = data.toString().match(/Duration: (\d{2}:\d{2}:\d{2}\.\d{2})/); if (match && match[1]) { const parts = match[1].split(':'); duration = parseInt(parts[0]) * 3600 + parseInt(parts[1]) * 60 + parseFloat(parts[2]); } }); proc.on('close', (code) => { if (code === 0 && duration > 0) { resolve(duration); } else { reject(new Error('Failed to get audio duration')); } }); }); } export async function splitAudioFile(filePath: string, maxFileSize_MB: number): Promise { const maxFileSize_Bytes = maxFileSize_MB * 1024 * 1024; const fileSize = fs.statSync(filePath).size; if (fileSize <= maxFileSize_Bytes) { return [filePath]; // No splitting needed } // Calculate duration per part based on file size ratio const duration = await getAudioDuration(filePath); // Uses FFmpeg const partDuration = (maxFileSize_Bytes / fileSize) * duration; const partFiles: string[] = []; let startTime = 0; while (startTime < duration) { const partFilePath = `${filePath.replace('.mp3', '')}_part${partFiles.length + 1}.mp3`; await new Promise((resolve, reject) => { const proc = spawn(ffmpeg, [ '-i', filePath, '-ss', startTime.toFixed(2), '-t', partDuration.toFixed(2), '-c', 'copy', '-y', partFilePath ]); proc.on('close', code => code === 0 ? resolve() : reject()); }); partFiles.push(partFilePath); startTime += partDuration; } return partFiles; // Example: ['meeting_part1.mp3', 'meeting_part2.mp3', 'meeting_part3.mp3'] } ``` -------------------------------- ### Transcribe Audio with OpenAI Whisper Source: https://context7.com/ericstrohmaier/discord-meeting-transcribe-summary/llms.txt Transcribes an array of audio file parts using OpenAI's Whisper model with built-in retry logic for transient network errors. It concatenates transcriptions from all parts and cleans up temporary split files afterwards. Requires an OpenAI API key. ```typescript import fs from 'fs'; import OpenAI from 'openai'; export async function transcribe(audioParts: string[]): Promise { const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); let fullTranscription = ''; for (const filePath of audioParts) { let attempt = 0; const maxRetries = 3; while (attempt < maxRetries) { attempt++; try { const transcription = await openai.audio.transcriptions.create({ file: fs.createReadStream(filePath), model: 'whisper-1', language: 'de' }); fullTranscription += transcription.text + ' '; break; } catch (e) { if (attempt >= maxRetries) throw e; const backoff = 1000 * Math.pow(2, attempt - 1); await new Promise(resolve => setTimeout(resolve, backoff)); } } // Cleanup temporary split files if (audioParts.length > 1) fs.unlinkSync(filePath); } return fullTranscription.trim(); } ``` -------------------------------- ### TypeScript Interfaces for Bot State and Meeting Data Source: https://context7.com/ericstrohmaier/discord-meeting-transcribe-summary/llms.txt Defines TypeScript interfaces for managing bot state, meeting data, and audio configurations. These interfaces ensure type safety and structure for the bot's internal data management, covering meeting properties, bot operational status, and audio recording parameters. ```typescript // TypeScript type definitions interface Meeting { name: string; recorded: boolean; transcribed: boolean; summarized: boolean; } interface BotState { currentMeeting: string | null; connection: VoiceConnection | null; recordingProcess: ChildProcess | null; mixingInterval: NodeJS.Timeout | null; userBuffers: Map | null; userStreams: Map | null; meetings: Meeting[]; } interface AudioSettings { channels: 1; // Mono audio rate: 48000; // 48kHz sample rate frameSize: 960; // Opus frame size bitrate: '64k'; // Audio bitrate mixInterval: 20; // Mix every 20ms } ``` -------------------------------- ### Send Meeting Artifacts to Discord Channel Source: https://context7.com/ericstrohmaier/discord-meeting-transcribe-summary/llms.txt Retrieves and sends meeting recordings (MP3/OGG), transcriptions (TXT), or summaries (MD) to the current Discord channel. Supports autocomplete for selecting meeting names. It reads files from a local directory structure. ```typescript import fs from 'fs'; import path from 'path'; // Usage: /meeting send summary Weekly Team Sync // Options: recording (MP3/OGG), transcription (TXT), summary (MD) const meetingName = interaction.options.getString('name', true); const what = interaction.options.getString('what', true); // 'recording' | 'summary' | 'transcription' const meetingPath = `./meetings/${meetingName}`; // Find requested file let fileToSend; const files = fs.readdirSync(meetingPath); if (what === 'recording') { const audioFiles = files.filter(f => f.endsWith('.mp3') || f.endsWith('.ogg')); fileToSend = path.join(meetingPath, audioFiles[0]); await interaction.editReply({ files: [fileToSend] }); } else if (what === 'summary') { const mdFiles = files.filter(f => f.endsWith('.md')); fileToSend = path.join(meetingPath, mdFiles[0]); const content = fs.readFileSync(fileToSend, 'utf-8'); // Post summary in thread const message = await interaction.fetchReply(); const thread = await message.startThread({ name: `Summary of the meeting '${meetingName}'` }); await sendSummary(content, thread); } else if (what === 'transcription') { const txtFiles = files.filter(f => f.endsWith('.txt')); fileToSend = path.join(meetingPath, txtFiles[0]); await interaction.editReply({ files: [fileToSend] }); } // Autocomplete handler for meeting names export async function autocomplete(interaction) { const focusedValue = interaction.options.getFocused().toLowerCase(); const choices = state.meetings.map(m => m.name); const filtered = choices .filter(choice => choice.toLowerCase().startsWith(focusedValue)) .slice(0, 25); await interaction.respond(filtered.map(c => ({ name: c, value: c }))); } ``` -------------------------------- ### Stop Meeting and Process Recording Source: https://context7.com/ericstrohmaier/discord-meeting-transcribe-summary/llms.txt Stops a meeting recording, converts audio from OGG to MP3, splits large audio files into manageable chunks (due to Whisper API's 24MB limit), transcribes the audio using the Whisper API, generates a summary with GPT-4o, and posts the results in a Discord thread. It handles audio conversion, transcription retries, and summary formatting. ```typescript import fs from 'fs'; import path from 'path'; import OpenAI from 'openai'; import config from 'config'; // Usage: /meeting stop // Cleanup recording resources await cleanupRecording(); // Stops FFmpeg, closes voice connection const meetingName = state.currentMeeting; const meetingPath = `./meetings/${meetingName}`; const oggPath = `${meetingPath}/${meetingName}.ogg`; const mp3Path = `${meetingPath}/${meetingName}.mp3`; // Convert OGG to MP3 await new Promise((resolve, reject) => { const ffmpegProcess = spawn(ffmpeg, [ '-i', oggPath, '-codec:a', 'libmp3lame', '-q:a', '2', '-y', mp3Path ]); ffmpegProcess.on('close', (code) => code === 0 ? resolve() : reject()); }); // Split large files (Whisper API has 24MB limit) const audioParts = await splitAudioFile(mp3Path, 8); // 8MB chunks // Returns: ['meeting.mp3'] or ['meeting_part1.mp3', 'meeting_part2.mp3', ...] // Transcribe each part with retry logic const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); let fullTranscription = ''; for (const filePath of audioParts) { const transcription = await openai.audio.transcriptions.create({ file: fs.createReadStream(filePath), model: 'whisper-1', language: 'de' }); fullTranscription += transcription.text + ' '; } fs.writeFileSync(`${meetingPath}/${meetingName}.txt`, fullTranscription); // Generate summary with GPT-4o const summary = await openai.chat.completions.create({ model: 'gpt-4o', messages: [ { role: 'system', content: config.openai.system_content.join('') }, { role: 'user', content: `${config.openai.user_content}${fullTranscription}` } ] }); const summaryText = summary.choices[0].message.content; fs.writeFileSync(`${meetingPath}/${meetingName}.md`, summaryText); // Post summary to Discord thread const message = await interaction.fetchReply(); const thread = await message.startThread({ name: `Summary of the meeting '${meetingName}'` }); await sendSummary(summaryText, thread); // Handles 2000 char limit ``` -------------------------------- ### List All Meetings in Discord Thread Source: https://context7.com/ericstrohmaier/discord-meeting-transcribe-summary/llms.txt Displays a list of all stored meetings and their processing status (recorded, transcribed, summarized) in a formatted table. The output is sent within a Discord thread, with the table enclosed in a code block to preserve formatting. It handles Discord's 2000 character limit for messages. ```typescript import state from '../../utils/state'; // Usage: /meeting list await interaction.reply(':arrow_down: Meetings list'); const message = await interaction.fetchReply(); const thread = await message.startThread({ name: 'Meetings list' }); // Format meeting status table const formattedMeetings = state.meetings.map((meeting) => { const name = meeting.name.padEnd(20, ' '); const recorded = meeting.recorded ? '+' : '-'; const transcribed = meeting.transcribed ? '+' : '-'; const summarized = meeting.summarized ? '+' : '-'; return ` ${name} | ${recorded.padEnd(8)} | ${transcribed.padEnd(11)} | ${summarized}`; }); const messageText = ` Meeting name | Recorded | Transcribed | Summarized ----------------------|----------|-------------|------------ ${formattedMeetings.join(' ')} `; // Send in code block, handling Discord's 2000 char limit await thread.send('```' + messageText + '```'); // Example output: // Meeting name | Recorded | Transcribed | Summarized // ---------------------|----------|-------------|------------ // Weekly Team Sync | + | + | + // Sprint Planning | + | + | - // Architecture Review | + | - | - ``` -------------------------------- ### Delete Meeting Recordings or Entire Meetings Source: https://context7.com/ericstrohmaier/discord-meeting-transcribe-summary/llms.txt Removes specific meeting recordings (audio files) or entire meeting directories from the file system. It also updates the bot's internal state to reflect the deletions. Uses Node.js file system modules. ```typescript import fs from 'fs'; import path from 'path'; // Usage: /meeting delete meeting Weekly Team Sync // Usage: /meeting delete recording Weekly Team Sync const meetingName = interaction.options.getString('name', true); const what = interaction.options.getString('what', true); // 'recording' | 'meeting' const meetingPath = `./meetings/${meetingName}`; if (what === 'recording') { // Delete only audio files const files = fs.readdirSync(meetingPath); const audioFiles = files.filter(f => f.endsWith('.ogg') || f.endsWith('.mp3')); audioFiles.forEach(file => fs.unlinkSync(path.join(meetingPath, file))); // Update state const meeting = state.meetings.find(m => m.name === meetingName); if (meeting) meeting.recorded = false; await interaction.editReply({ embeds: [fileDeletedEmbed] }); } else { // Delete entire meeting directory fs.rmSync(meetingPath, { recursive: true, force: true }); // Remove from state state.meetings = state.meetings.filter(m => m.name !== meetingName); await interaction.editReply({ embeds: [meetingDeletedEmbed] }); } ``` -------------------------------- ### Send Long Messages to Discord Source: https://context7.com/ericstrohmaier/discord-meeting-transcribe-summary/llms.txt Sends potentially long messages to a Discord channel, respecting the 2000-character limit by splitting the message into multiple parts if necessary. This ensures that summaries or other long outputs are delivered completely. ```typescript export async function sendSummary(message: string, channel): Promise { const lines = message.split('\n'); let chunk = ''; for (const line of lines) { if ((chunk + '\n' + line).length > 2000) { await channel.send(chunk); chunk = line; } else { chunk += (chunk ? '\n' : '') + line; } } if (chunk) await channel.send(chunk); } ``` === COMPLETE CONTENT === This response contains all available snippets from this library. No additional content exists. Do not make further requests.