본문 바로가기

카테고리 없음

[n8n 인프런 챌린지] 4주차: AI 회의록 서기 만들기

시작하게 된 이유

벌써 인프런 n8n 챌린지 5주차네요!! 이번 챕터의 주제는 "회의록 STT 워크플로우" 였어요.

오디오 파일을 업로드하면 Gemini가 받아쓰고, 다시 LLM이 교정·요약해서, 구글 드라이브와 노션에 자동으로 정리해주는 흐름입니다.

사실 이번 주는 심화 미션 아이디어가 잘 떠오르지 않아서…ㅎㅎ 기본 미션에 좀 더 집중해보기로 했어요.

대신 그냥 따라 치기만 하는 게 아니라, "이 노드가 왜 이렇게 생겼지?" 를 좀 더 꼼꼼히 들여다보면서 만들어봤습니다.

회의록 자동화는 평소에도 정말 만들고 싶었던 거라, 따라 만들면서 "오 이거 진짜 회사에서 잘 쓰겠는데?" 싶더라구요~~ 오늘도 그 과정을 정리해서 공유해보겠습니다!

 

⚠️ 본문에 등장하는 폴더 ID, DB ID 등은 모두 일반화한 값입니다.


무엇을 만들었나

폼 페이지에 오디오 파일을 업로드하면, 자동으로 이렇게 흘러갑니다.

[오디오 업로드] → [Gemini로 음성 인식]
              → [텍스트 파일로 변환] → [구글 드라이브에 백업]
              → [LLM이 교정·윤문] → [LLM이 요약·구조화]
              → [노션 DB에 회의록 페이지 생성]

채택 미션 오디오(chap5_STT_example_audio.mp3)는 국회 본회의 일부였는데요, 잘 받아쓰는 것까진 좋았지만 STT 결과에 "사내를 선포합니다"(아마 "산회") 같은 명백한 오인식이 섞여 있더라구요. 이래서 정제 단계가 따로 필요하구나 싶었어요!


워크플로우 한눈에 보기

전체 구조는 이렇습니다!

노드 역할

1 On form submission 오디오 파일 받기
2 소진_google (Gemini) 음성 → 텍스트 (STT)
3 Convert to File JSON 응답 → .txt
4 Upload file (Google Drive) 원본 전사본 백업
5 Check Type (Gemini) 1차 정제 (교정·윤문)
6 Summarize (Gemini) 2차 요약·구조화 (JSON)
7 Create a database page (Notion) 최종 회의록 적재

폼 하나 던지면 노션 페이지까지 일직선으로 흐르는 깔끔한 파이프라인이지요??


핵심 노드 살펴보기

1. On form submission — 폼 트리거

{
  "formTitle": "AI 회의록 비서",
  "formFields": { "values": [{ "fieldLabel": "data", "fieldType": "file" }] }
}

n8n이 자동으로 만들어주는 공개 URL에 접속하면 파일 업로드 UI가 떠요.

따로 프론트를 만들지 않아도 동료한테 링크만 던지면 끝나는 게 진짜 편리한 것 같아요!

처음엔 "왜 굳이 Form Trigger지? Webhook도 있잖아?" 싶었는데, 직접 만들어보니 Webhook으로 받으면 multipart/form-data 파싱이나 임시 저장을 직접 신경 써야 하더라구요.

Form Trigger는 data[0].filename처럼 정돈된 형태로 다음 노드에 바로 넘겨줘서 훨씬 편했어요!

 

2. Gemini STT — 음성 → 텍스트

{
  "resource": "audio",
  "modelId": "models/gemini-2.5-flash",
  "inputType": "binary"
}

여기가 핵심 첫 단계예요.

inputType: binary 의 의미가 처음엔 헷갈렸는데, 이전 노드(폼)에서 받은 파일을 base64 인코딩 없이 바이너리 그대로 Gemini에 넘긴다는 뜻이더라구요. n8n이 중간 변환을 알아서 처리해줘서 정말 편했습니다!

왜 gemini-2.5-flash일까? 처음엔 "Pro가 더 좋은 거 아닌가?" 싶었는데, 이 단계는 단순한 "들리는 대로 받아쓰기" 작업이라 정확성보다 속도와 비용이 더 중요해요. 정제는 다음 단계에서 더 똑똑한 모델이 맡으니까요!

 

3. Convert to File — JSON에서 텍스트만 꺼내기

{
  "operation": "toText",
  "sourceProperty": "content.parts[0].text",
  "fileName": "{{ $('On form submission').item.json.data[0].filename }}_회의록.txt"
}

Gemini 응답은 이런 구조로 와요.

{
  "content": {
    "parts": [
      { "text": "받아쓴 결과..." }
    ],
    "role": "model"
  }
}

parts가 배열인 이유는 응답에 텍스트 말고 다른 타입(함수 호출, 이미지 등)이 같이 들어올 수도 있어서래요.

우리는 텍스트만 필요하니까 parts[0].text로 첫 번째 텍스트 파트만 꺼냅니다!

 

4. Upload file — 구글 드라이브에 백업

여기서 처음엔 "어차피 노션에 들어갈 건데 왜 굳이 드라이브에도 저장하지?" 싶었어요. 근데 만들어 보고 곰곰이 생각해보니까 이유가 있더라구요!

  • 감사 추적: LLM이 정제하면서 내용이 변형될 수 있는데, "이 회의록 진짜 회의에서 나온 말 맞아?" 검증할 때 원본이 있으면 확인이 가능
  • 디버깅: 요약이 이상하게 나왔을 때 STT가 망친 건지 LLM이 망친 건지 추적 가능
  • 재실행: 요약 프롬프트만 바꿔서 다시 돌리고 싶을 때, STT부터 다시 안 돌려도 됨

데이터 파이프라인에서 각 단계 산출물을 저장해두는게 여러모로 좋을 것 같죠?!

 

5. Check Type — 1차 정제 (교정·윤문)

이 부분 시스템 프롬프트는 여기에서 전문을 볼 수 있어요!

당신은 전문적인 교정 및 편집 에디터입니다.

[작업 지침]
1. 교정 및 윤문: 맞춤법, 띄어쓰기, 문법 오류를 수정...
2. 길이 유지 (요약 금지): 내용을 요약하거나 축약하지 마세요...
3. 누락 방지: 텍스트의 시작부터 끝까지...
4. 문맥 수정: 음성 인식(STT) 과정에서 잘못 인식된 단어 수정...
5. 출력 형식: 교정이 완료된 텍스트만 출력하세요.

 

STT가 변환해준 텍스트에 오타가 있는 경우가 많은 것 같아요.

실제로 예제 오디오를 돌려보면 "산회를 선포합니다"가 "사내를 선포합니다"로 잡히는 식의 오인식이 섞여 있더라구요.

이전에 사용한 Gemini 노드를 사용하지만, 시스템 메시지로 "너는 교정만 해줘" 하고 지시를해서 역할을 명확히 정해주는 것이 중요한 것 같습니다!

 

6. Summarize — 구조화된 JSON 만들기

두 번째 LLM 호출. 출력을 JSON으로 강제시킨 게 핵심이에요.

[출력 포맷 - JSON]
{
  "meeting_date": "YYYY-MM-DD",
  "meeting_title": "회의 주제",
  "meeting_oneline": "한줄 요약",
  "meeting_attendee": ["참석자1", "참석자2"],
  "meeting_summary": "..."
}

왜 JSON으로 만드냐면,

다음 단계가 노션이기때문이에요!

노션 DB는 "회의 제목", "회의 날짜", "한줄요약" 같은 구조화된 필드를 따로따로 받아야 해서, LLM이 자연어로만 답하면 우리가 또 파싱해야 해요. 처음부터 JSON으로 받으면 다음 노드에서 JSON.parse(...).meeting_title 한 줄로 끝!

 

방법은 단순히 jsonOutput: true 옵션을 켜면 모델이 순수 JSON만 출력합니다! 쉽죠?!

 

7. Create a database page — 노션에 적재

{
  "propertiesUi": {
    "propertyValues": [
      { "key": "회의 제목|title", "title": "{{ JSON.parse(...).meeting_title }}" },
      { "key": "회의 날짜|date", "date": "{{ JSON.parse(...).meeting_date }}" }
    ]
  }
}

key가 회의 제목|title 형태로 적힌 게 처음엔 신기했어요.

알고 보니까 노션은 같은 이름의 필드를 다른 타입으로 여러 개 만들 수 있어서, n8n이 필드명과 타입을 같이 전달해야 정확히 매핑된다고 하더라구요!


실행 결과

 

아 근데 액션아이템이 왤케 웃긴지ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ쿠ㅜㅜ


만들면서 느낀 점

1. 중간 산출물을 남기는 습관의 중요성

원본 STT 텍스트를 굳이 드라이브에 따로 저장하는 게 처음엔 비효율적으로 보였는데, 직접 만들어보니까 디버깅할 때 진짜 든든하더라구요. LLM 파이프라인은 블랙박스가 되기 쉬워서, 각 단계 결과를 어딘가에 남기는 게 운영의 핵심이라는 걸 느꼈어요.

2. LLM 단계는 책임을 쪼개는 게 낫다

"한 번에 다 시키지 말고 정제 따로, 요약 따로"라는 패턴이 진짜 효과가 있더라구요! 1·2주차에서 배운 관심사 분리가 LLM 워크플로우에도 그대로 적용되는 것 같아요.

3. 시스템 프롬프트는 또 다른 코드

"요약 금지", "추측 금지", "출력 형식 강제" 같은 규칙들을 프롬프트에 박아두는 게 그냥 페르소나 설정이 아니라 비즈니스 룰을 자연어로 명시한 명세서라는 느낌이 들었어요. 3주차에서 운영 비서 만들 때도 비슷한 느낌이었는데, 회의록 워크플로우에서도 똑같이 적용되더라구요!

4. 노드 하나의 디테일이 결과를 좌우한다

파일명에 원본 이름 붙이기, JSON 출력 강제, 노션 필드명에 타입 명시, JSON.parse 까먹지 않기… 작은 디테일 하나하나가 결과 품질에 직결되는 게 신기했어요. n8n은 노코드 같지만 알수록 디테일에 신경 써야 한다는 걸 느꼈습니다!


다음에 도전해보고 싶은 것 (심화 미션 아이디어)

이번엔 기본 미션만 했지만, 만들고 나니까 살을 더 붙이고 싶은 부분들이 보이더라구요!

  • 트리거 변경: 폼 말고 슬랙으로 음성 메모 던지면 자동 실행되게
  • 요약 포맷 커스텀: 회사 회의 양식에 맞춰서 "결정사항 / 후속 액션 / 책임자" 섹션 추가
  • 저장소 추가: 노션 + 슬랙 알림 ("회의록 정리됐어요!")
  • 액션 아이템만 따로 빼서 ClickUp이나 Linear 같은 작업 관리 도구에 자동 등록

챌린지 끝나고 시간 나면 이 중에서 하나 골라서 확장해볼 생각이에요~~


마치며

이번 챕터는 음성 → 텍스트 → LLM 처리 → 외부 도구 연동 이라는 패턴을 한 번에 다 경험할 수 있어서 정말 알찼던 것 같아요!

평소에 "회의록 작성 자동화" 라는 거 인터넷에서 많이 봤지만 막연하게만 알고 있었는데, 직접 만들어보니까 "아 이게 이런 식으로 돌아가는 거구나" 하고 감이 잡혔어요.

심화 미션은 못 했지만, 기본 미션을 깊이 들여다본 것도 충분히 가치 있는 한 주였다고 생각해요ㅎㅎ (라고 스스로 위로해봅니다…)

다음 주차도 기대됩니다! 이번 글도 읽어주셔서 감사합니다 :)