80줄의 Python으로 전화 통화에서 서식 있는 이메일까지 — Telnyx를 활용한 AI 음성 메모 정리
요약
Telnyx API를 활용하여 단 80줄의 Python 코드로 음성 메모를 구조화된 이메일로 변환하는 Flask 웹훅 구현 방법을 소개합니다. 별도의 복잡한 인프라 없이 하나의 API 키만으로 통화 처리, AI 전사 정리, 이메일 전송을 통합 수행합니다.
핵심 포인트
- Telnyx API 하나로 음성, AI 추론, 메시징 통합 관리 가능
- Flask 기반의 가벼운 상태 머신 아키텍처 구현
- 비구조화된 음성 전사 데이터를 구조화된 JSON 및 이메일로 변환
- DB나 Redis 없이 인메모리 방식으로 간단한 프로토타입 구축
AI 음성 메모를 이메일로 — 전화 통화를 받고, 음성 메모를 수집하며, AI 추론 (AI Inference)을 통해 문법을 교정하고 구조를 추출한 뒤, 서식 있는 이메일을 전달하는 80줄짜리 Flask 웹훅 (webhook)입니다. 음성, AI, 메시징을 위한 단 하나의 API 키만 필요합니다. 제3자 서비스는 필요 없습니다.
음성 메모 문제 (The Voice Memo Problem)
음성 메모는 생각을 포착하는 가장 빠른 방법입니다. 말만 하면 끝이니까요. 하지만 결과물은 나중에 (본인을 포함해) 아무도 읽고 싶지 않은 중구난방인 오디오 덩어리일 뿐입니다. 가공되지 않은 전사 (transcript) 데이터는 더 심각합니다. 문장 부호도 없고, 말실수, 추임새, 구조가 전혀 없습니다. 이메일, 상태 업데이트, 또는 회의 요약으로 활용하려면 여전히 수동으로 정리해야 합니다.
기존 솔루션들은 이 문제를 여러 서비스로 분산시킵니다. 전사 (transcription) 서비스가 오디오를 텍스트로 변환하고, LLM API가 텍스트를 정리하며, 이메일 서비스가 결과를 전송합니다. 세 개의 벤더, 세 개의 API 키, 세 개의 청구서, 그리고 세 개의 장애 지점 (points of failure)이 발생합니다.
AI 음성 메모를 이메일로 변환하는 이 예제는 하나의 네트워크에서 이 모든 것을 수행합니다. Telnyx Call Control이 전화 통화를 처리하고, Telnyx AI Inference가 전사 (transcript)를 정리하며, Telnyx Messaging이 이메일을 전달합니다. 단 하나의 API 키, 하나의 Flask 파일, 그리고 약 80줄의 Python 코드면 충분합니다.
기능 (What It Does)
Telnyx 번호로 전화를 겁니다. 앱이 전화를 받아 인사를 건네고 듣기 시작합니다. 상태 업데이트, 회의 요약, 버그 보고 등 원하는 메모를 구술한 뒤, 완료되면 #를 누릅니다. 앱은 전사 (transcript) 데이터를 구조화된 JSON을 반환하는 프롬프트와 함께 AI Inference로 전송합니다. 이 JSON에는 제목, 서식 있는 본문, 그리고 실행 항목 (action items) 목록이 포함됩니다. 앱은 이를 사용자의 기본 주소로 이메일 전송하며, 전화로 다음과 같이 확인해 줍니다: "메모가 저장되어 이메일로 발송되었습니다. 제목: [추론된 제목]. 안녕히 계세요!"
| 단계 | 이벤트 | 작업 |
|---|---|---|
| 1 | call.initiated (수신) | 전화 받기, 세션 생성 |
| ... |
메모는 메모리에 저장되어 GET /memos를 통해 접근할 수 있습니다. 따라서 이메일 전송이 설정되지 않았더라도 서식 있는 메모를 여전히 가져올 수 있습니다.
아키텍처 (The Architecture)
모든 것이 하나의 Flask 파일 안에 존재합니다. 데이터베이스(Database), Redis, Celery도 필요 없습니다. 통화 상태(Call state)는 call_control_id를 키로 사용하는 인메모리 딕셔너리(in-memory dict)에서 추적됩니다. 메모(Memos)는 리스트(list)에 저장됩니다. 백그라운드 스레드(background thread)는 5분마다 만료된 세션을 정리합니다 (1시간 TTL).
발신자가 귀하의 Telnyx 번호로 전화를 겁니다
↓
Telnyx가 call.initiated 웹훅(webhook)을 전송 → /webhooks/voice
...
통화 흐름 상태 머신 (The Call Flow State Machine)
웹훅 핸들러(webhook handler)는 Telnyx 이벤트에 의해 구동되는 상태 머신(state machine)입니다. 각 이벤트는 다음 동작을 트리거합니다:
@app.route("/webhooks/voice", methods=["POST"])
def handle_voice():
# 이벤트를 신뢰하기 전에 Telnyx Ed25519 서명을 검증합니다.
...
상태 머신에는 이벤트당 하나씩, 총 다섯 가지의 전이(transitions)가 있습니다. call.initiated 핸들러는 발신(outbound) 통화 구간을 처리하지 않도록 direction == "incoming"을 확인합니다. call.speak.ended 핸들러는 인사(greeting) 단계에서 정보 수집(gathering) 단계로 진행시키는 역할을 합니다. Telnyx는 TTS 재생이 완료될 때 이 이벤트를 발생시키므로, 수집(gather)이 시작되기 전에 발신자가 인사를 들었음을 알 수 있습니다.
수집(gather) 단계에서는 end_silence_timeout_secs=5를 사용합니다. 발신자가 5초 동안 말을 멈추면 수집이 자동으로 종료됩니다. timeout_secs=120은 전체 수집 시간을 2분으로 제한합니다. terminating_digit="#" 설정을 통해 발신자가 '#' 키를 눌러
Temperature(온도)는 0.3입니다. 이는 동일한 메모가 매번 거의 동일한 출력을 생성할 수 있을 만큼 충분히 낮으면서도, AI가 내용으로부터 합리적인 제목을 추론할 수 있을 만큼 충분히 높습니다. max_tokens=400 제한은 일반적인 음성 메모에 충분합니다.
만약 AI 응답이 유효한 JSON 형식이 아니라면, except 블록이 원문 음성(raw speech)을 저장하고 더 단순한 확인 메시지를 읽어줍니다. 발신자는 이메일은 받지 못하더라도 메모가 저장되었다는 사실은 여전히 확인할 수 있습니다:
except Exception:
memos.append({"raw": speech, "caller": call["caller"],
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ")})
...
Graceful degradation(우아한 성능 저하) — 전화 통화는 결코 낭비되지 않습니다. AI가 실패하면 원문 전사(transcript)가 보존됩니다. 이메일 전송이 실패하면 서식이 지정된 메모가 보존됩니다. 발신자는 항상 확인 메시지를 받게 됩니다.
Telnyx Messaging을 통한 이메일 전송
메모의 서식이 지정된 후, 앱은 Telnyx Messaging API를 통해 이를 이메일로 전송합니다:
def send_email(to, subject, body):
try:
requests.post("https://api.telnyx.com/v2/messages",
...
전화를 받고 AI 추론(inference)을 실행하는 것과 동일한 TELNYX_API_KEY가 이메일도 전송합니다. 하나의 키, 하나의 청구서, 하나의 네트워크를 사용하는 것입니다. 이메일 전송은 try/except로 감싸져 있는데, 이는 이메일 전송에 추가적인 Telnyx 설정이 필요할 수 있기 때문입니다. 전송에 실패하더라도 메모는 여전히 저장되며 GET /memos를 통해 검색할 수 있습니다.
Webhook 서명 검증 (Webhook Signature Verification)
모든 Telnyx 웹훅(webhook)은 Ed25519 키로 서명됩니다. 앱은 이벤트를 처리하기 전에 서명을 검증합니다:
try:
client.webhooks.unwrap(request.get_data(as_text=True), headers=dict(request.headers))
except Exception:
...
Telnyx Python SDK의 webhooks.unwrap() 메서드는 내부적으로 Ed25519 검증을 처리합니다. 이 메서드는 telnyx-signature-ed25519 및 telnyx-timestamp 헤더를 읽고, 서명된 페이로드(payload)를 재구성하며, 공개 키(public key)를 통해 서명을 검증합니다. 이때 파싱된 JSON이 아닌 원문 바디(raw body)를 검증하는데, 이는 JSON 파싱이 정형화(canonical)되어 있지 않아 서명 검증이 실패할 수 있기 때문입니다.
직접 시도해 보세요
git clone https://github.com/team-telnyx/telnyx-code-examples.git
cd telnyx-code-examples/ai-voice-memo-to-email-python
cp .env.example .env # TELNYX_API_KEY, MEMO_NUMBER, DEFAULT_EMAIL 추가
...
그 다음:
ngrok http 5000
Telnyx Portal에서 Call Control Application의 webhook URL을 https://<id>.ngrok.io/webhooks/voice로 설정하세요.
Telnyx 번호로 전화를 겁니다. 메모를 말합니다. #을 누릅니다. 이메일을 확인합니다.
저장된 메모 확인:
curl http://localhost:5000/memos | python3 -m json.tool
주요 링크:
- Repo: https://github.com/team-telnyx/telnyx-code-examples/tree/main/ai-voice-memo-to-email-python
- Telnyx Portal: https://portal.telnyx.com
- Call Control 문서: https://developers.telnyx.com/docs/voice/call-control
- AI Inference 문서: https://developers.telnyx.com/docs/inference
- Messaging 문서: https://developers.telnyx.com/docs/messaging
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기