03. Audio-in-LLM 직접 호출 v2
이 노트북은 오디오 파일을 base64로 인코딩해 Chat Completions API의 input_audio로 직접 전달하고, 모델의 음성 응답도 파일로 저장합니다.
# Colab 또는 로컬 노트북 실행 환경을 구분하기 위해 sys를 가져옵니다.
import sys
# 패키지 설치 명령을 현재 Python 커널에서 실행하기 위해 subprocess를 가져옵니다.
import subprocess
# 패키지 설치 여부를 확인하기 위해 importlib.util을 가져옵니다.
import importlib.util
# 환경 변수에서 API 키를 읽고 설정하기 위해 os를 가져옵니다.
import os
# API 키를 화면에 노출하지 않고 입력받기 위해 getpass를 가져옵니다.
import getpass
# .env 파일 위치를 다루기 위해 pathlib의 Path를 가져옵니다.
from pathlib import Path
# google.colab 모듈이 있으면 현재 런타임이 Google Colab이라고 판단합니다.
IN_COLAB = "google.colab" in sys.modules
# 노트북에서 사용하는 import 이름과 pip 패키지 이름을 짝지어 둡니다.
REQUIRED_PACKAGES = {
"openai": "openai>=2.26.0",
"dotenv": "python-dotenv>=1.2.2",
"pydantic": "pydantic>=2.11.0",
"PIL": "pillow>=12.1.1",
"requests": "requests>=2.32.5",
"numpy": "numpy>=2.3.3",
"websockets": "websockets>=15.0.1",
"websocket": "websocket-client>=1.8.0",
"nest_asyncio": "nest-asyncio>=1.6.0",
}
def ensure_package(import_name: str, package_name: str) -> None:
if importlib.util.find_spec(import_name) is not None:
return
subprocess.check_call(
[sys.executable, "-m", "pip", "install", "-q", package_name]
)
if IN_COLAB:
for import_name, package_name in REQUIRED_PACKAGES.items():
ensure_package(import_name, package_name)
from dotenv import load_dotenv
load_dotenv(Path.cwd() / ".env")
if not os.getenv("OPENAI_API_KEY"):
secret_key = None
try:
from google.colab import userdata
secret_key = userdata.get("OPENAI_API_KEY")
except Exception:
secret_key = None
if secret_key:
os.environ["OPENAI_API_KEY"] = secret_key
else:
entered_key = getpass.getpass("OPENAI_API_KEY를 입력하세요: ").strip()
if entered_key:
os.environ["OPENAI_API_KEY"] = entered_key
if not os.getenv("OPENAI_API_KEY"):
raise RuntimeError(
"OPENAI_API_KEY가 없습니다. Colab Secrets, 수동 입력, 또는 .env 파일로 설정해 주세요."
)
print(f"개발환경: {'Colab' if IN_COLAB else '로컬'}")
print("OPENAI_API_KEY 준비 완료")
개발환경: 로컬
OPENAI_API_KEY 준비 완료
실습 전에 보는 핵심 개념과 API
핵심 개념
- 이 노트북은 전용 STT API나 TTS API가 아니라 Chat Completions API 안에서 오디오 입력과 오디오 출력을 함께 다룹니다.
- 오디오 입력은 보통 Base64 문자열로 인코딩한 뒤 messages 안의 input_audio 블록으로 전달합니다.
- 출력에서 modalities에 audio를 포함하면 텍스트 답변과 함께 음성 응답도 받을 수 있습니다.
API 소개
- client.chat.completions.create(...)는 텍스트 전용이 아니라 멀티모달 메시지도 처리할 수 있습니다.
- 이 노트북은 오디오 입력, 오디오 출력, 그리고 둘을 함께 묶은 대화형 흐름을 단계적으로 보여줍니다.
API 호출부와 주요 매개변수
response = client.chat.completions.create(
model="gpt-audio-mini",
messages=[
{
"role": "user",
"content": [
{"type": "text", "text": "이 음성을 요약해 주세요."},
{
"type": "input_audio",
"input_audio": {"data": audio_b64, "format": "mp3"},
},
],
}
],
modalities=["text", "audio"],
audio={"voice": "alloy", "format": "mp3"},
)
- model
- 보통 gpt-audio-mini처럼 오디오 입출력을 지원하는 채팅 모델을 넣습니다.
- 기본값: 실질적인 기본값이 없으므로 명시하는 것이 안전합니다.
- messages
- 일반 채팅과 같은 대화 배열이지만, content 안에 텍스트 블록과 오디오 블록을 함께 넣을 수 있습니다.
- 오디오를 이해해야 하는 요청에서는 이 구조를 명시적으로 읽는 것이 중요합니다.
- 기본값: 없습니다.
- input_audio.data
- Base64로 인코딩한 오디오 본문입니다.
- 파일 경로를 직접 주는 것이 아니라, 실제 오디오 바이트를 문자열로 넣는 구조라는 점이 핵심입니다.
- 기본값: 없습니다.
- input_audio.format
- 예를 들면 mp3, wav 같은 입력 오디오 형식을 지정합니다.
- 실제 바이트 형식과 맞지 않으면 해석이 어긋날 수 있으므로 정확히 적어야 합니다.
- 기본값: 없습니다.
- modalities
- 가능한 값은 보통 text, audio 조합입니다.
- ['text']로 두면 텍스트 답변만, ['text', 'audio']로 두면 텍스트와 오디오를 함께 받을 수 있습니다.
- 기본값: 생략하면 텍스트 중심 응답으로 생각하는 편이 안전합니다.
- audio
- 출력 음성의 voice, format 같은 설정을 담습니다.
- 예를 들어 {"voice": "alloy", "format": "mp3"}처럼 지정합니다.
- 기본값: 오디오 출력을 받으려면 실질적으로 명시하는 것이 안전합니다.
- audio.voice
- TTS와 비슷하게 alloy, ash, ballad, coral, echo, sage, shimmer, verse, marin, cedar 같은 내장 목소리를 고려할 수 있습니다.
- 기본값: 실질적인 기본값이 없으므로 명시하는 것이 일반적입니다.
- audio.format
- 자주 쓰는 값은 mp3, wav 등입니다.
- 파일 저장과 재생 편의성을 고려해 선택합니다.
- 기본값: 실습에서는 항상 명시하는 편이 안전합니다.
요청/응답 필드에서 볼 것
- 이 실습에서는 요청의 messages 구조를 먼저 읽고, 텍스트 블록과 input_audio 블록이 같은 턴 안에 어떻게 들어가는지 눈으로 확인하는 것이 가장 중요합니다.
- 입력 오디오가 제대로 들어갔는지 확인하려면 Base64 길이와 파일 경로를 먼저 출력해 보고, 실제로 저장한 입력 MP3가 재생되는지도 같이 확인해 보세요.
- 응답을 확인할 때는 message.content의 텍스트 답변과 message.audio.data, message.audio.transcript를 분리해서 보고, 텍스트와 음성이 같은 의미를 말하는지도 비교하는 것이 좋습니다.
- 결과가 이상하면 가장 먼저 input_audio.format, modalities, audio.voice, audio.format이 요청에 원하는 값으로 들어갔는지 다시 보면 됩니다.
- 실습 로그에는 입력 오디오 파일, 출력 오디오 파일, 텍스트 답변, 오디오 transcript를 함께 저장해 두면 멀티모달 응답 흐름을 다시 설명하기 쉬워집니다.
1단계. 실행 환경 준비
오디오 입력 LLM은 텍스트와 오디오를 같은 메시지에 넣습니다. 준비 셀에서 모델과 파일 경로를 확인합니다.
# 운영체제 환경 변수를 읽기 위해 os 모듈을 가져옵니다.
import os
# JSON 데이터를 저장하고 보기 좋게 출력하기 위해 json 모듈을 가져옵니다.
import json
# 오디오 base64 데이터를 다루기 위해 base64 모듈을 가져옵니다.
import base64
# 실행 시간을 측정하기 위해 time 모듈을 가져옵니다.
import time
# 비동기 Realtime 예제에서 이벤트 루프를 사용하기 위해 asyncio를 가져옵니다.
import asyncio
# 파일 경로를 안전하게 다루기 위해 Path를 가져옵니다.
from pathlib import Path
# .env 파일의 환경 변수를 읽기 위해 load_dotenv를 가져옵니다.
from dotenv import load_dotenv
# OpenAI API를 직접 호출하기 위해 OpenAI 클라이언트를 가져옵니다.
from openai import OpenAI
# 현재 실행 위치를 저장합니다.
CURRENT_DIR = Path.cwd()
# 루트에서 실행하는 경우 audio 트랙 폴더를 찾습니다.
if (CURRENT_DIR / "06.audio").exists():
# 워크스페이스 루트에서 실행 중이면 06.audio를 실습 루트로 사용합니다.
AUDIO_ROOT = CURRENT_DIR / "06.audio"
# 하위 폴더에서 실행하는 경우 경로 조각에서 06.audio를 찾습니다.
elif "06.audio" in [part for part in CURRENT_DIR.parts]:
# 06.audio까지의 경로를 복원합니다.
AUDIO_ROOT = Path(*CURRENT_DIR.parts[: CURRENT_DIR.parts.index("06.audio") + 1])
# 그 밖의 경우 상대 경로를 사용합니다.
else:
# 현재 위치 기준 06.audio를 실습 루트로 사용합니다.
AUDIO_ROOT = CURRENT_DIR / "06.audio"
# 워크스페이스 루트는 audio 트랙의 상위 폴더입니다.
WORKSPACE_ROOT = AUDIO_ROOT.parent
# v2 결과 저장 폴더를 정합니다.
OUTPUT_DIR = AUDIO_ROOT / "output" / "v2"
# 결과 저장 폴더를 생성합니다.
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
# 워크스페이스 루트의 .env 파일을 읽습니다.
load_dotenv(WORKSPACE_ROOT / ".env")
# API 키가 없으면 명확한 오류로 중단합니다.
if not os.getenv("OPENAI_API_KEY"):
# 교육생이 바로 확인할 수 있도록 환경 변수 이름을 알려 줍니다.
raise RuntimeError("OPENAI_API_KEY가 없습니다. 워크스페이스 루트의 .env 파일을 확인해 주세요.")
# OpenAI API 클라이언트를 생성합니다.
client = OpenAI()
# 음성 인식 모델명을 환경 변수에서 읽습니다.
TRANSCRIBE_MODEL = os.getenv("OPENAI_TRANSCRIBE_MODEL", "gpt-4o-mini-transcribe")
# 음성 합성 모델명을 환경 변수에서 읽습니다.
TTS_MODEL = os.getenv("OPENAI_TTS_MODEL", "gpt-4o-mini-tts")
# 오디오 입출력 LLM 모델명을 환경 변수에서 읽습니다.
AUDIO_LLM_MODEL = os.getenv("OPENAI_AUDIO_LLM_MODEL", "gpt-audio-mini")
# Realtime API 모델명을 환경 변수에서 읽습니다.
REALTIME_MODEL = os.getenv("OPENAI_REALTIME_MODEL", "gpt-realtime-mini")
# 워크스페이스 기준 상대 경로를 반환하는 함수를 정의합니다.
def workspace_path(path: Path) -> str:
# 절대 경로를 워크스페이스 기준 / 구분 문자열로 변환합니다.
return path.resolve().relative_to(WORKSPACE_ROOT.resolve()).as_posix()
# 환경 정보를 출력합니다.
print(f"오디오 실습 폴더: {AUDIO_ROOT}")
# 결과 저장 폴더를 출력합니다.
print(f"결과 저장 폴더: {OUTPUT_DIR}")
# 사용할 음성 인식 모델을 출력합니다.
print(f"STT 모델: {TRANSCRIBE_MODEL}")
# 사용할 음성 합성 모델을 출력합니다.
print(f"TTS 모델: {TTS_MODEL}")
오디오 실습 폴더: c:\Users\USER\Desktop\AS.1.1\d02\06.audio
결과 저장 폴더: c:\Users\USER\Desktop\AS.1.1\d02\06.audio\output\v2
STT 모델: gpt-4o-mini-transcribe
TTS 모델: gpt-4o-mini-tts
2단계. 입력 오디오 생성과 base64 인코딩
API에는 로컬 파일 경로가 아니라 오디오 바이트를 base64 문자열로 전달합니다. 이 변환 과정을 직접 보면 input_audio 구조가 훨씬 분명해집니다.
# LLM에 입력할 오디오 파일 경로를 정합니다.
input_audio_path = OUTPUT_DIR / "03-02_audio_llm_input.mp3"
# 입력 오디오로 만들 문장을 준비합니다.
input_text = "서울에서 OpenAI API를 배우는 초보자를 위해 오늘 배운 내용을 한 문장으로 요약해 주세요."
# TTS API로 입력 오디오를 생성합니다.
with client.audio.speech.with_streaming_response.create(
# TTS 모델을 지정합니다.
model=TTS_MODEL,
# 음성 목소리를 지정합니다.
voice="alloy",
# 오디오로 만들 문장을 전달합니다.
input=input_text,
# MP3 형식으로 저장합니다.
response_format="mp3",
) as response:
# 생성된 음성을 파일로 저장합니다.
response.stream_to_file(input_audio_path)
# 오디오 파일 바이트를 읽습니다.
audio_bytes = input_audio_path.read_bytes()
# 오디오 바이트를 base64 문자열로 인코딩합니다.
audio_base64 = base64.b64encode(audio_bytes).decode("utf-8")
# 입력 오디오 경로를 출력합니다.
print(f"입력 오디오: {workspace_path(input_audio_path)}")
# base64 문자열 길이를 출력합니다.
print(f"base64 길이: {len(audio_base64)}")
입력 오디오: 06.audio/output/v2/03-02_audio_llm_input.mp3
base64 길이: 150528
3단계. 오디오 입력과 오디오 출력 요청
이 호출에서 핵심은 content 배열 안의 input_audio와, 응답 형식을 정하는 modalities 및 audio 매개변수입니다. 모델이 들은 내용의 transcript와 생성한 음성을 함께 확인합니다.
# Chat Completions API를 직접 호출합니다.
chat_response = client.chat.completions.create(
# 오디오 입출력을 지원하는 모델을 지정합니다.
model=AUDIO_LLM_MODEL,
# 텍스트와 오디오 응답을 모두 요청합니다.
modalities=["text", "audio"],
# 오디오 응답의 목소리와 형식을 지정합니다.
audio={"voice": "alloy", "format": "mp3"},
# 사용자 메시지에 텍스트 지시와 입력 오디오를 함께 넣습니다.
messages=[
{
# 사용자 발화임을 나타냅니다.
"role": "user",
# 텍스트 지시와 오디오 입력 블록을 같은 content 배열에 담습니다.
"content": [
# 모델이 수행해야 할 작업을 텍스트로 설명합니다.
{
"type": "text",
"text": "이 음성을 듣고 한국어로 짧게 답한 뒤, 같은 내용을 음성으로도 말해 주세요.",
},
# base64 인코딩한 MP3 오디오를 입력 블록으로 전달합니다.
{
"type": "input_audio",
"input_audio": {"data": audio_base64, "format": "mp3"},
},
],
}
],
)
# 첫 번째 선택지를 꺼냅니다.
choice = chat_response.choices[0]
# 메시지 객체를 꺼냅니다.
message = choice.message
# 텍스트 응답을 읽습니다.
text_answer = message.content or ""
# 오디오 응답 객체를 읽습니다.
audio_answer = message.audio
# 오디오 응답 base64 데이터를 읽습니다.
answer_audio_base64 = (
# SDK 객체 속성이 있으면 data 속성을 우선 사용합니다.
audio_answer.data
if hasattr(audio_answer, "data")
# dict 형태 응답이면 data 키를 사용합니다.
else audio_answer["data"]
)
# 모델이 생성한 오디오 transcript를 읽습니다.
answer_transcript = (
# SDK 객체 속성이 있으면 transcript 속성을 우선 사용합니다.
audio_answer.transcript
if hasattr(audio_answer, "transcript")
# dict 형태 응답이면 transcript 키를 사용하고, 없으면 빈 문자열로 둡니다.
else audio_answer.get("transcript", "")
)
# 오디오 응답 저장 경로를 정합니다.
answer_audio_path = OUTPUT_DIR / "03-03_audio_llm_answer.mp3"
# 응답 오디오를 파일로 저장합니다.
answer_audio_path.write_bytes(base64.b64decode(answer_audio_base64))
# 텍스트와 transcript를 함께 저장합니다.
(OUTPUT_DIR / "03-03_audio_llm_answer.json").write_text(
json.dumps(
{
# 화면에 보여 줄 텍스트 답변을 저장합니다.
"text_answer": text_answer,
# 생성된 음성에 대응하는 transcript를 저장합니다.
"audio_transcript": answer_transcript,
# 저장된 오디오 파일 경로를 기록합니다.
"audio_path": workspace_path(answer_audio_path),
},
ensure_ascii=False,
indent=2,
)
+ "\n",
encoding="utf-8",
)
# 텍스트 응답을 출력합니다.
print(text_answer)
# 오디오 transcript를 출력합니다.
print(answer_transcript)
# 오디오 파일 위치를 출력합니다.
print(f"응답 오디오: {workspace_path(answer_audio_path)}")
좋습니다. 오늘은 OpenAI API의 기본 개념, API 키 생성, 간단한 요청 보내는 방법을 배웠다고 요약할 수 있습니다.
지금 바로 음성으로 말씀드리겠습니다.
응답 오디오: 06.audio/output/v2/03-03_audio_llm_answer.mp3
'AI System > OpenAI API와 바이브 코딩으로 배우는 AI 서비스 개발' 카테고리의 다른 글
| d02 - 06. Audio - 04. voice agent - step1. basic connection (0) | 2026.06.02 |
|---|---|
| d02 - 06. Audio - 04. voice agent - README.md (0) | 2026.06.02 |
| d02 - 06. Audio - 02.tts (0) | 2026.06.02 |
| d02 - 06. Audio - 01. stt (0) | 2026.06.02 |
| d02 - 05. Image - 04. workflow (0) | 2026.06.02 |