회의록
회의록 (Meeting minutes)
이 튜토리얼에서는 자동화된 회의록 생성기를 만듭니다. 애플리케이션이 회의 녹음을 텍스트로 옮기고, 논의를 요약하고, 핵심 요점과 액션 아이템을 추출하고, 감정을 분석한 다음, 결과를 Word 문서로 저장해요.
출처: 문서
본문
시작하기
이 튜토리얼은 지원 언어 중 하나에 익숙하고 OpenAI API 키가 있다고 가정해요. 짧은 스모크 테스트 오디오 파일 또는 최대 25MB의 자체 녹음을 사용할 수 있어요.
언어에 맞는 OpenAI SDK와 DOCX 라이브러리를 설치하세요:
- JavaScript:
docx - Python:
python-docx - Go:
godocx - Java: Apache POI XWPF
- Ruby:
caracal
오디오 텍스트 변환
첫 번째 단계는 회의 녹음을 /v1/audio API로 전달하는 것이에요. 현재 파일 텍스트 변환 모델은 음성 언어를 쓰여진 텍스트로 변환해요. 먼저 선택적 prompt와 temperature 파라미터를 생략하고 기본값을 사용하세요.
샘플 오디오 다운로드
다운로드한 파일을 예시를 실행하는 디렉터리에 meeting.wav로 저장하거나, meeting.wav를 녹음 경로로 바꾸세요. 짧은 다운로드 가능한 클립은 워크플로를 검증해요. 유용한 요약과 액션 아이템을 만들려면 최대 25MB의 실제 회의 녹음을 사용하세요.
녹음을 열고 파일 내용을 gpt-transcribe에 보내는 헬퍼를 정의하세요:
import fs from "node:fs";
import { Document, HeadingLevel, Packer, Paragraph, TextRun } from "docx";
import OpenAI from "openai";
const openai = new OpenAI();
async function transcribeAudio(audioFilePath) {
const transcription = await openai.audio.transcriptions.create({
file: fs.createReadStream(audioFilePath),
model: "gpt-transcribe",
});
return transcription.text;
}
from pathlib import Path
from docx import Document
from openai import OpenAI
client = OpenAI()
def transcribe_audio(audio_file_path: str | Path) -> str:
with Path(audio_file_path).open("rb") as audio_file:
transcription = client.audio.transcriptions.create(
file=audio_file,
model="gpt-transcribe",
)
return transcription.text
package main
import (
"context"
"fmt"
"os"
"strings"
"github.com/gomutex/godocx"
"github.com/openai/openai-go/v3"
)
type meetingMinutes struct {
AbstractSummary string
KeyPoints string
ActionItems string
Sentiment string
}
var client = openai.NewClient()
func transcribeAudio(ctx context.Context, audioFilePath string) (string, error) {
audioFile, err := os.Open(audioFilePath)
if err != nil {
return "", err
}
defer audioFile.Close()
transcription, err := client.Audio.Transcriptions.New(ctx, openai.AudioTranscriptionNewParams{
File: audioFile,
Model: "gpt-transcribe",
})
if err != nil {
return "", err
}
return transcription.Text, nil
}
import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.models.audio.transcriptions.TranscriptionCreateParams;
import com.openai.models.chat.completions.ChatCompletionCreateParams;
import java.io.IOException;
import java.io.OutputStream;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Path;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFStyle;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTStyle;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STStyleType;
public final class TutorialMeetingMinutesExample {
private TutorialMeetingMinutesExample() {}
record MeetingMinutes(
String abstractSummary, String keyPoints, String actionItems, String sentiment) {}
private static final class ClientHolder {
private static final OpenAIClient INSTANCE = OpenAIOkHttpClient.fromEnv();
}
private static OpenAIClient client() {
return ClientHolder.INSTANCE;
}
static String transcribeAudio(Path audioFilePath) {
var transcription =
client()
.audio()
.transcriptions()
.create(
TranscriptionCreateParams.builder()
.file(audioFilePath)
.model("gpt-transcribe")
.build());
return transcription.asTranscription().text();
}
require "caracal"
require "openai"
require "pathname"
client = OpenAI::Client.new
def transcribe_audio(client, audio_file_path)
transcription = client.audio.transcriptions.create(
file: Pathname(audio_file_path),
model: "gpt-transcribe"
)
transcription.text
end
헬퍼는 로컬 오디오 경로를 받아 언어의 표준 파일 API로 파일을 열고 파일 내용을 텍스트 변환 모델에 전달해요. 텍스트 변환 엔드포인트는 로컬 경로나 원격 URL이 아니라 오디오 바이트가 필요해요. 서버가 녹음을 다른 곳에 저장한다면 텍스트 변환을 만들기 전에 녹음을 요청으로 다운로드하거나 스트리밍하세요.
GPT 모델로 트랜스크립트 요약·분석
Chat Completions API를 통해 트랜스크립트를 GPT 모델로 전달하세요. 이 튜토리얼은 기존 통합에 대해 여전히 지원되는 Chat Completions 경로를 보여줘요. 새 프로젝트에는 Responses API를 사용하고 gpt-6-astra로 시작하세요. 아래 스니펫은 테스트된 모델을 사용해 요약을 생성하고, 핵심 요점과 액션 아이템을 추출하고, 감정을 분석해요.
이 튜토리얼은 각 작업에 별도의 모델 호출을 사용해요. 지시를 하나의 요청으로 결합해 호출을 줄일 수 있지만, 별도의 프롬프트는 각 결과를 조정하기 더 쉽게 만들어요.
트랜스크립트와 작업별 지시를 모델에 보내는 공유 헬퍼를 정의하세요:
async function complete(transcription, instructions) {
const response = await openai.chat.completions.create({
model: "gpt-5.5",
messages: [
{ role: "system", content: instructions },
{ role: "user", content: transcription },
],
});
return response.choices[0].message.content ?? "";
}
def complete(transcription: str, instructions: str) -> str:
response = client.chat.completions.create(
model="gpt-5.5",
messages=[
{"role": "system", "content": instructions},
{"role": "user", "content": transcription},
],
)
return response.choices[0].message.content or ""
func complete(ctx context.Context, transcription, instructions string) (string, error) {
response, err := client.Chat.Completions.New(ctx, openai.ChatCompletionNewParams{
Model: "gpt-5.5",
Messages: []openai.ChatCompletionMessageParamUnion{
openai.SystemMessage(instructions),
openai.UserMessage(transcription),
},
})
if err != nil {
return "", err
}
return response.Choices[0].Message.Content, nil
}
private static String complete(String transcription, String instructions) {
var response =
client()
.chat()
.completions()
.create(
ChatCompletionCreateParams.builder()
.model("gpt-5.5")
.addSystemMessage(instructions)
.addUserMessage(transcription)
.build());
return response.choices().get(0).message().content().orElse("");
}
def complete(client, transcription, instructions)
response = client.chat.completions.create(
model: "gpt-5.5",
messages: [
{
role: :system,
content: instructions
},
{
role: :user,
content: transcription
}
]
)
response.choices.first.message.content || ""
end
회의록의 네 섹션을 반환하는 오케스트레이션 헬퍼를 정의하세요:
async function buildMeetingMinutes(transcription) {
return {
"Abstract summary": await extractAbstractSummary(transcription),
"Key points": await extractKeyPoints(transcription),
"Action items": await extractActionItems(transcription),
Sentiment: await analyzeSentiment(transcription),
};
}
def meeting_minutes(transcription: str) -> dict[str, str]:
return {
"Abstract summary": abstract_summary_extraction(transcription),
"Key points": key_points_extraction(transcription),
"Action items": action_item_extraction(transcription),
"Sentiment": sentiment_analysis(transcription),
}
func buildMeetingMinutes(ctx context.Context, transcription string) (meetingMinutes, error) {
summary, err := extractAbstractSummary(ctx, transcription)
if err != nil {
return meetingMinutes{}, err
}
keyPoints, err := extractKeyPoints(ctx, transcription)
if err != nil {
return meetingMinutes{}, err
}
actionItems, err := extractActionItems(ctx, transcription)
if err != nil {
return meetingMinutes{}, err
}
sentiment, err := analyzeSentiment(ctx, transcription)
if err != nil {
return meetingMinutes{}, err
}
return meetingMinutes{summary, keyPoints, actionItems, sentiment}, nil
}
static MeetingMinutes buildMeetingMinutes(String transcription) {
return new MeetingMinutes(
extractAbstractSummary(transcription),
extractKeyPoints(transcription),
extractActionItems(transcription),
analyzeSentiment(transcription));
}
def build_meeting_minutes(client, transcription)
{
"Abstract summary" => extract_abstract_summary(client, transcription),
"Key points" => extract_key_points(client, transcription),
"Action items" => extract_action_items(client, transcription),
"Sentiment" => analyze_sentiment(client, transcription)
}
end
헬퍼는 트랜스크립트를 요약, 핵심 요점, 액션 아이템, 감정 각각에 하나씩 네 개의 집중된 헬퍼로 전달해요. 애플리케이션이 더 많은 분석이 필요하면 다른 헬퍼와 출력 섹션을 추가하세요.
각 함수가 작동하는 방식은 다음과 같아요:
요약 추출
요약 헬퍼는 중요한 결정과 맥락을 보존하면서 곁가지를 생략하는 하나의 간결한 문단을 모델에 요청해요. 시스템 메시지가 이 동작을 제어해요. 결과를 구성하는 더 많은 방법은 프롬프트 엔지니어링 가이드를 참고하세요.
async function extractAbstractSummary(transcription) {
return complete(
transcription,
"Summarize the meeting transcript in one concise paragraph. Keep the most important decisions and context, and omit tangents."
);
}
def abstract_summary_extraction(transcription: str) -> str:
return complete(
transcription,
"Summarize the meeting transcript in one concise paragraph. "
"Keep the most important decisions and context, and omit tangents.",
)
func extractAbstractSummary(ctx context.Context, transcription string) (string, error) {
return complete(ctx, transcription, "Summarize the meeting transcript in one concise paragraph. Keep the most important decisions and context, and omit tangents.")
}
static String extractAbstractSummary(String transcription) {
return complete(
transcription,
"Summarize the meeting transcript in one concise paragraph. "
+ "Keep the most important decisions and context, and omit tangents.");
}
def extract_abstract_summary(client, transcription)
complete(
client,
transcription,
"Summarize the meeting transcript in one concise paragraph. Keep the most important decisions and context, and omit tangents."
)
end
핵심 요점 추출
핵심 요점 헬퍼는 회의에서 논의된 중요한 아이디어, 발견, 주제를 나열해요. 관련 프로젝트나 회사 맥락을 시스템 메시지에 추가하면 모델이 청중에게 중요한 것을 식별하는 데 도움이 돼요.
async function extractKeyPoints(transcription) {
return complete(
transcription,
"List the most important ideas, findings, and topics from the meeting. Use concise bullet points."
);
}
def key_points_extraction(transcription: str) -> str:
return complete(
transcription,
"List the most important ideas, findings, and topics from the meeting. "
"Use concise bullet points.",
)
func extractKeyPoints(ctx context.Context, transcription string) (string, error) {
return complete(ctx, transcription, "List the most important ideas, findings, and topics from the meeting. Use concise bullet points.")
}
static String extractKeyPoints(String transcription) {
return complete(
transcription,
"List the most important ideas, findings, and topics from the meeting. "
+ "Use concise bullet points.");
}
def extract_key_points(client, transcription)
complete(
client,
transcription,
"List the most important ideas, findings, and topics from the meeting. Use concise bullet points."
)
end
액션 아이템 추출
액션 아이템 헬퍼는 작업과 후속 조치를 식별하고, 트랜스크립트가 제공하면 담당자와 마감일을 포함해요. 다른 시스템에서 작업을 만들고 할당하려면 이 단계를 함수 호출에 연결하세요.
async function extractActionItems(transcription) {
return complete(
transcription,
"List every task or follow-up agreed to in the meeting. Include the owner and deadline when the transcript provides them."
);
}
def action_item_extraction(transcription: str) -> str:
return complete(
transcription,
"List every task or follow-up agreed to in the meeting. "
"Include the owner and deadline when the transcript provides them.",
)
func extractActionItems(ctx context.Context, transcription string) (string, error) {
return complete(ctx, transcription, "List every task or follow-up agreed to in the meeting. Include the owner and deadline when the transcript provides them.")
}
static String extractActionItems(String transcription) {
return complete(
transcription,
"List every task or follow-up agreed to in the meeting. "
+ "Include the owner and deadline when the transcript provides them.");
}
def extract_action_items(client, transcription)
complete(
client,
transcription,
"List every task or follow-up agreed to in the meeting. Include the owner and deadline when the transcript provides them."
)
end
감정 분석
감정 헬퍼는 논의를 긍정, 부정 또는 중립으로 분류하고 평가를 설명해요. 더 단순한 작업에는 더 낮은 비용과 지연으로 품질 목표를 충족하는지 확인하기 위해 gpt-5.6-terra를 시도해 보세요.
async function analyzeSentiment(transcription) {
return complete(
transcription,
"Describe the meeting's overall sentiment as positive, negative, or neutral, and briefly explain the assessment."
);
}
def sentiment_analysis(transcription: str) -> str:
return complete(
transcription,
"Describe the meeting's overall sentiment as positive, negative, or "
"neutral, and briefly explain the assessment.",
)
func analyzeSentiment(ctx context.Context, transcription string) (string, error) {
return complete(ctx, transcription, "Describe the meeting's overall sentiment as positive, negative, or neutral, and briefly explain the assessment.")
}
static String analyzeSentiment(String transcription) {
return complete(
transcription,
"Describe the meeting's overall sentiment as positive, negative, or neutral, "
+ "and briefly explain the assessment.");
}
def analyze_sentiment(client, transcription)
complete(
client,
transcription,
"Describe the meeting's overall sentiment as positive, negative, or neutral, and briefly explain the assessment."
)
end
회의록 내보내기
배포할 수 있는 읽기 가능한 형식으로 회의록을 저장하세요. Microsoft Word는 이런 종류의 보고서에 흔한 선택이에요. 예시는 각 언어에 맞는 DOCX 라이브러리를 사용해요. 엔드투엔드 애플리케이션에서는 결과를 이메일로 보내거나 다른 시스템에 쓰는 대신 사용할 수 있어요.
각 결과 섹션을 Word 문서에 쓰는 헬퍼를 정의하세요:
async function saveAsDocx(minutes, filename) {
const children = Object.entries(minutes).flatMap(([heading, content]) => [
new Paragraph({ text: heading, heading: HeadingLevel.HEADING_1 }),
new Paragraph({
children: content
.split(/\r\n?|\n/)
.flatMap((line, index) => [
...(index > 0 ? [new TextRun({ break: 1 })] : []),
new TextRun(line),
]),
}),
]);
const document = new Document({ sections: [{ children }] });
await fs.promises.writeFile(filename, await Packer.toBuffer(document));
}
def save_as_docx(minutes: dict[str, str], filename: Path) -> None:
document = Document()
for heading, content in minutes.items():
document.add_heading(heading, level=1)
document.add_paragraph(content)
document.save(filename)
func saveAsDocx(minutes meetingMinutes, filename string) error {
document, err := godocx.NewDocument()
if err != nil {
return err
}
for _, section := range []struct{ heading, content string }{
{"Abstract summary", minutes.AbstractSummary},
{"Key points", minutes.KeyPoints},
{"Action items", minutes.ActionItems},
{"Sentiment", minutes.Sentiment},
} {
document.AddHeading(section.heading, 1)
for _, line := range strings.Split(strings.ReplaceAll(section.content, "\r\n", "\n"), "\n") {
document.AddParagraph(line)
}
}
return document.SaveTo(filename)
}
static void saveAsDocx(MeetingMinutes minutes, Path filename) throws IOException {
try (var document = new XWPFDocument();
OutputStream output = Files.newOutputStream(filename)) {
addHeadingStyle(document);
addSection(document, "Abstract summary", minutes.abstractSummary());
addSection(document, "Key points", minutes.keyPoints());
addSection(document, "Action items", minutes.actionItems());
addSection(document, "Sentiment", minutes.sentiment());
document.write(output);
}
}
private static void addHeadingStyle(XWPFDocument document) {
var headingStyle = CTStyle.Factory.newInstance();
headingStyle.setStyleId("Heading1");
headingStyle.addNewName().setVal("Heading 1");
headingStyle.setType(STStyleType.PARAGRAPH);
headingStyle.addNewPPr().addNewOutlineLvl().setVal(BigInteger.ZERO);
document.createStyles().addStyle(new XWPFStyle(headingStyle));
}
private static void addSection(XWPFDocument document, String heading, String content) {
var headingParagraph = document.createParagraph();
headingParagraph.setStyle("Heading1");
var headingRun = headingParagraph.createRun();
headingRun.setBold(true);
headingRun.setFontSize(16);
headingRun.setText(heading);
var contentRun = document.createParagraph().createRun();
String[] lines = content.split("\\R", -1);
for (int index = 0; index < lines.length; index += 1) {
if (index > 0) contentRun.addBreak();
contentRun.setText(lines[index]);
}
}
def save_as_docx(minutes, filename)
Caracal::Document.save(filename) do |document|
minutes.each do |heading, content|
document.h1(heading)
content.split(/\r\n?|\n/, -1).each { |line| document.p(line) }
end
end
end
헬퍼는 생성된 섹션과 출력 파일명을 받아 각 섹션에 제목과 문단을 추가하고 현재 작업 디렉터리에 문서를 저장해요.
마지막으로 오디오 파일에서 회의록을 생성하는 단계를 결합하세요:
const transcription = await transcribeAudio("meeting.wav");
const minutes = await buildMeetingMinutes(transcription);
console.log(minutes);
await saveAsDocx(minutes, "meeting_minutes.docx");
audio_file_path = Path("meeting.wav")
transcription = transcribe_audio(audio_file_path)
minutes = meeting_minutes(transcription)
print(minutes)
save_as_docx(minutes, Path("meeting_minutes.docx"))
func main() {
ctx := context.Background()
transcription, err := transcribeAudio(ctx, "meeting.wav")
if err != nil {
panic(err)
}
minutes, err := buildMeetingMinutes(ctx, transcription)
if err != nil {
panic(err)
}
fmt.Printf("%+v\n", minutes)
if err := saveAsDocx(minutes, "meeting_minutes.docx"); err != nil {
panic(err)
}
}
public static void main(String[] args) throws IOException {
String transcription = transcribeAudio(Path.of("meeting.wav"));
MeetingMinutes minutes = buildMeetingMinutes(transcription);
System.out.println(minutes);
saveAsDocx(minutes, Path.of("meeting_minutes.docx"));
}
}
transcription = transcribe_audio(client, "meeting.wav")
minutes = build_meeting_minutes(client, transcription)
puts minutes
save_as_docx(minutes, "meeting_minutes.docx")
이 코드는 프로세스 작업 디렉터리에서 meeting.wav를 해석하고, 회의록을 생성·출력하고, 이를 meeting_minutes.docx로 저장해요.
이제 기본적인 회의록 워크플로가 생겼습니다. 프롬프트 엔지니어링으로 프롬프트를 조정하거나 함수 호출로 엔드투엔드 시스템을 구축하세요.