음성봇을 스텁부터 만든 이유 — 테스트로 설계를 먼저 굳히다

시리즈 · 음성봇 직접 만들기 — 실시간 음성봇의 뼈대를 바닥부터 만든 8편 기록입니다. 1. 끼어들기(barge-in) — 재생 중인 TTS를 실시간으로 끊는 법 2. "말이 끝났다"는 판단 — VAD 엔드포인터 3. 엔진을 코드 수정 없이 바꾸는 인터페이스 설계 4. 로컬 LLM 대화 두뇌와 도구 호출 루프 5. 매 턴 지연시간을 재는 이유 6. 브라우저 마이크에서 서버까지 — 오디오 프로토콜 7. 로컬 Whisper STT 연결하기 8. 스텁부터 만든 이유 — 테스트 우선 설계 (완결) — 지금 보는 글 돌아보며 시작하는 마지막 편 이 시리즈를 시작할 때부터 정해 둔 규칙이 하나 있었습니다. 진짜 음성 인식 모델과 진짜 합성 모델이 준비되기 전까지는, 그 둘을 흉내만 내는 가짜 엔진으로 전체 구조를 먼저 돌려본다는 규칙입니다. 여덟 편을 거치며 끼어들기 처리, 상태 흐름, 프로토콜, 엔진 인터페이스를 하나씩 다뤘는데, 그 모든 것이 사실 이 가짜 엔진 위에서 검증됐습니다. 지금 코드를 다시 열어 보면 그때는 별생각 없이 세운 규칙이 결과적으로 시리즈 전체의 진행 속도를 결정했다는 게 눈에 들어옵니다. 이번 편에서는 왜 굳이 가짜부터 만들었는지, 그리고 그 선택이 시리즈 전체에 어떤 영향을 남겼는지 정리하면서 이 시리즈를 마무리하려 합니다. 실제 모델을 기다리지 않기로 한 이유 음성봇의 핵심 로직은 사실 음성 인식이나 음성 합성 자체가 아닙니다. 사용자가 말을 시작했는지 끝냈는지 판단하는 상태머신, 합성 중에 끼어들기가 들어왔을 때 스트림을 멈추는 동시성 처리, 이벤트가 순서대로 오가는 흐름 제어 같은 것들이 훨씬 까다롭습니다. 반면 실제 인식·합성 모델은 무겁고 설치가 까다롭고 실행 속도도 느립니다. 이 두 가지를 한꺼번에 붙잡고 있으면 어려운 동시성 버그를 실제 모델의 로딩 시간과 뒤섞어 디버깅하게 됩니다. 모델이 느려서 반응이 늦은 건지, 상태머신이 잘못 짜여서 늦은 건지 구분이 안 되는 상황을 미...

인스타 저장한 릴스·카드뉴스 노션에 텍스트로 자동 정리하는 법

인스타 릴스·카드뉴스가 한국어 텍스트로 정제돼 Notion에 적재된 모습
인스타 릴스·카드뉴스가 한국어 텍스트로 정제돼 Notion에 적재된 모습 (사용자 본인 실제 화면)

인스타그램 노션 자동 저장 봇을 직접 만들면 저장함에 쌓이기만 하던 콘텐츠가 검색 가능한 텍스트 자산이 됩니다. 인스타그램에서 저장(북마크)해 둔 릴스와 카드뉴스는 다시 보기가 어렵습니다. 앱 안에 묻히고, 영상은 다시 재생해야 하고, 이미지 속 텍스트는 검색되지 않습니다. 이 봇은 Discord 채널에 인스타 URL만 올리면 릴스 음성은 Whisper로 전사하고, 이미지·캡처는 Claude 비전으로 읽어 한국어 텍스트로 정제한 뒤 Notion에 박제합니다. 본인은 하루 1~10개씩 저장하던 인스타 콘텐츠를 이 봇으로 누적 [N]건+ 텍스트 아카이브로 만들었습니다.

여기서는 Discord URL → instaloader → Claude 비전/Whisper → Notion으로 이어지는 파이프라인을 처음부터 하나씩 짜 봅니다. macOS + Python + discord.py + instaloader + ffmpeg + Whisper + Claude Code CLI + Notion 조합이고, 본인이 쓰고 있는 그대로라 한 시간 남짓이면 같은 환경을 다시 세울 수 있습니다. 직전 편 Discord 구글 캘린더 봇, Discord 유튜브 봇 → Notion 아카이브와 같은 "Discord = 인박스" 패턴의 인스타 버전입니다.

[주의] Instagram은 비공식 API(instaloader)를 사용하므로 약관상 회색지대입니다. 이 글은 본인이 저장한 콘텐츠를 개인 소량(하루 1~10개)으로 아카이브하는 용도이며, 대량 스크래핑이나 재배포는 다루지 않습니다. 부계정과 낮은 빈도 사용을 권장합니다.

1. 왜 이 조합인가

핵심 발상은 단순합니다. Discord는 무엇이든 던져 넣는 인박스, Notion은 영구 아카이브, 그 사이를 Claude가 자동으로 잇는다는 것입니다. 사람은 인스타 URL 한 줄을 채널에 붙여넣기만 하면 됩니다.

인스타그램 저장함이 블랙홀이 되는 이유는 분명합니다. 릴스의 핵심 정보는 영상 음성에 있고, 카드뉴스의 정보는 이미지 안 텍스트에 있는데 둘 다 검색이 되지 않습니다. 그래서 이 봇은 콘텐츠 유형별로 다르게 처리합니다. 릴스·영상은 ffmpeg로 오디오를 추출해 Whisper로 한국어 전사하고, 이미지·카드뉴스·캐러셀은 Claude 비전으로 읽어 OCR과 요약을 합니다. 캡션과 해시태그는 원문 그대로 보존합니다.

저장도 Claude에게 위임합니다. Claude Code CLI가 정제 결과를 받아 Notion에 직접 페이지를 만들고, 첫 이미지를 Cover로 설정합니다. 이때 캡션을 그대로 옮기는 데서 그치지 않고, 본인이 나중에 다시 읽기 좋은 구조로 정리하는 단계를 한 번 거칩니다.

2. 사전 준비물 체크리스트

  • macOS, Python 3.11+, Discord 계정과 전용 채널 1개
  • Instagram 계정(부계정 권장)과 instaloader
  • ffmpeg(릴스 오디오 추출)와 Whisper(small 모델, 한국어)
  • Claude Code CLI와 Notion Integration(또는 Notion MCP)
  • SQLite(macOS 기본 포함)

3. 단계별 구현

3.1 Discord 봇 생성 + 전용 채널

Discord Developer Portal에서 봇을 만들고 Message Content Intent를 켭니다. OAuth2로 서버에 초대한 뒤 인스타 전용 채널을 만들고 채널 ID를 복사합니다. 토큰과 채널 ID는 config.yaml 또는 .env에만 저장합니다.

3.2 instaloader 세션 로그인 (차단 회피 핵심)

이 봇에서 가장 중요한 단계입니다. 매 요청마다 로그인하면 Instagram이 인증 코드(challenge)를 요구하거나 계정을 일시 차단합니다. 그래서 한 번만 로그인해 세션 파일을 저장하고, 이후에는 그 세션을 재사용합니다.


pip install instaloader
python3 -c "import instaloader; L=instaloader.Instaloader(); L.login('부계정아이디','비밀번호'); L.save_session_to_file()"

이후 봇은 load_session_from_file로 세션을 불러오고, 실패할 때만 다시 로그인합니다. 부계정을 쓰고 하루 1~10개 수준으로만 처리하면 차단 위험이 크게 줄어듭니다.

3.3 추출 — 큐에 넣고 30분 주기로 처리

봇은 on_message로 인스타 URL(/p/, /reel/, /tv/)을 감지해 shortcode를 파싱하고 SQLite 큐에 행을 추가합니다. 실제 처리는 별도 process_queue.py가 launchd 30분 주기로 큐를 비웁니다. 한 번에 최대 5건, 실패하면 최대 3회 재시도합니다. 봇은 큐에 넣기만 하고 무거운 처리는 분리돼 있어, URL이 몰려도 봇이 멈추지 않습니다.

3.4 변환 — 비전 OCR + Whisper 전사

convert_content.py가 포스트 유형별로 분기합니다.

  • 이미지: Claude 비전으로 OCR과 요약
  • 캐러셀: 각 이미지를 개별 비전 처리한 뒤 순서대로 통합
  • 릴스·영상: ffmpeg로 오디오를 추출해 Whisper(small, 한국어)로 전사
  • 캡션·해시태그: 원문 그대로 보존

영어 콘텐츠는 한국어로 번역해 정제하고, 한국어 콘텐츠는 그대로 일목요연하게 정리합니다.

3.5 저장 — Notion에 Cover와 함께 분할 업로드

Discord에 인스타 URL 입력 후 Notion 저장 완료 알림 화면
Discord에 인스타 URL을 올리면 Notion 저장 완료 알림이 오는 화면 (사용자 본인 실제 화면)

정제 결과는 Claude Code CLI(claude -p)에 넘기고, Claude가 정해진 규칙을 읽어 Notion에 직접 저장합니다. 첫 이미지를 Cover로 설정하고, 본문은 Notion의 100블록 제한 때문에 섹션별로 나눠 올립니다. 저장이 끝나면 Discord에 노션 링크가 담긴 완료 알림이 옵니다.

4. 자주 만나는 에러 3가지

4.1 instaloader 로그인이 막히거나 세션이 만료됨

매 요청마다 로그인하면 Instagram이 인증 코드를 요구하거나 계정을 일시 차단합니다. 세션 파일을 저장해 재사용하고, 부계정과 하루 1~10개 저빈도 처리로 위험을 낮춥니다. 세션이 만료되면 한 번 다시 로그인해 세션을 갱신합니다.

4.2 릴스 한국어 Whisper 전사 품질이 떨어짐

small 모델은 빠르지만 한국어 고유명사나 전문용어를 오인식하고, 배경음악이 큰 릴스에서는 정확도가 더 떨어집니다. 속도와 정확도를 보고 모델 크기를 조정하고, 캡션·해시태그로 보완한 뒤 Claude 정제 단계에서 교정합니다.

4.3 정제 결과가 Notion 100블록 제한을 넘어 저장이 거부됨

긴 캐러셀이나 릴스 정제 결과가 body.children should be ≤ 100 blocks 에러로 거부됩니다. 본문을 섹션별로 나눠 올리고 실패한 섹션만 재시도하며, 정제 단계에서 500줄 이내로 제한합니다.

마치며

인스타 저장함에 묻혀 다시 보지 않던 릴스와 카드뉴스가, 이제는 검색 한 번으로 디테일까지 돌아오는 텍스트 자산이 되었습니다. 영상을 다시 재생하지 않아도, 이미지를 다시 열지 않아도 핵심이 vault에 남아 있습니다.

이 글의 봇은 Discord 콘텐츠 아카이브 시리즈의 한 편입니다. 유튜브 영상·일정 캡처·뉴스 URL 같은 변형 봇은 다른 편에서 다룹니다. 본문 중 명령어가 동작하지 않으면 instaloader 문서, Whisper 저장소, Claude Code CLI 문서를 함께 확인하기를 권합니다.

댓글

이 블로그의 인기 게시물

디스코드 구글캘린더 연동 봇 만들기: 카톡 캡처 OCR 자동 등록 시스템 구축

클로드 코드 비용 줄이는 법 — ollama 로컬 LLM 연동 (M3 맥북)

유튜브 노션 자동 저장 봇 만들기 — Claude CLI MCP + Discord 233일 운영기