Realtime 대화
Realtime 대화 (Realtime conversations)
WebRTC나 WebSocket으로 Realtime API에 연결했다면, Realtime 모델(예: gpt-realtime-2.1)을 호출해 음성 대 음성 대화를 할 수 있어요. 그러려면 동작을 시작하려고 클라이언트 이벤트를 보내고, Realtime API가 취한 동작에 반응하려고 서버 이벤트를 들어야 해요.
출처: 문서
본문
이 가이드는 오디오·텍스트 생성, 이미지 입력, 함수 호출 같은 모델 기능을 쓰기 위한 이벤트 흐름과, Realtime Session의 상태를 어떻게 생각해야 하는지 안내해요. 모델과 대화할 필요가 없다면, 즉 응답이 필요 없다면 Realtime API를 transcription 모드로 쓸 수 있어요.
Realtime 음성 대 음성 세션
Realtime Session은 모델과 연결된 클라이언트 사이의 상태를 가진 상호작용이에요. 세션의 핵심 구성 요소는 다음과 같아요.
- Session 객체: 상호작용의 파라미터(사용 모델, 출력 생성에 쓰는 음성, 기타 설정)를 제어해요.
- Conversation(대화): 현재 세션 중 생성된 사용자 입력 Item과 모델 출력 Item을 나타내요.
- Responses: 모델이 생성한 오디오·텍스트 Item으로, Conversation에 추가돼요.
입력 오디오 버퍼와 WebSockets — WebRTC를 쓰면 모델과 오디오를 주고받는 데 필요한 미디어 처리 대부분이 WebRTC API가 도와줘요. WebSockets으로 오디오를 다룬다면 base64로 인코딩된 오디오를 담은 JSON 이벤트로 서버에 audio를 보내며 입력 오디오 버퍼를 직접 다뤄야 해요.
이 컴포넌트들이 모여 하나의 Realtime Session을 이뤄요. 클라이언트 이벤트로 세션 상태를 갱신하고, 서버 이벤트를 들어 세션 안의 상태 변화에 반응하게 돼요.
세션 수명주기 이벤트
WebRTC나 WebSockets으로 세션을 시작하면 서버가 session.created 이벤트를 보내 세션이 준비됐음을 알려줘요. 클라이언트는 session.update 이벤트로 현재 세션 설정을 갱신할 수 있어요. 대부분의 세션 속성은 언제든 갱신할 수 있지만, 세션 중 모델이 한 번 오디오로 응답한 뒤에는 오디오 출력에 쓰는 voice는 바꿀 수 없어요. Realtime 세션의 최대 길이는 60분이에요.
session.update 클라이언트 이벤트로 세션을 갱신하는 예시를 볼게요. 이 채널들로 클라이언트 이벤트를 보내는 방법은 WebRTC나 WebSocket 가이드를 참고하세요.
const event = {
type: "session.update",
session: {
type: "realtime",
model: "gpt-realtime-2.1",
// Lock the output to audio (set to ["text"] if you want text without audio)
output_modalities: ["audio"],
audio: {
input: {
format: {
type: "audio/pcm",
rate: 24000,
},
turn_detection: {
type: "semantic_vad",
},
},
output: {
format: {
type: "audio/pcm",
rate: 24000,
},
voice: "marin",
},
},
// Use a server-stored prompt by ID. Optionally pin a version and pass variables.
prompt: {
id: "pmpt_123", // your stored prompt ID
version: "89", // optional: pin a specific version
variables: {
city: "Paris", // example variable used by your prompt
},
},
// You can still set direct session fields; these override prompt fields if they overlap:
instructions:
"Speak clearly and briefly. Confirm understanding before taking actions.",
},
};
// WebRTC data channel and WebSocket both have .send()
dataChannel.send(JSON.stringify(event));
세션을 갱신하면 서버는 새 세션 상태를 담은 session.updated 이벤트를 내보내요.
| 관련 클라이언트 이벤트 | 관련 서버 이벤트 |
|---|---|
session.update |
session.created, session.updated |
텍스트 입력과 출력
Realtime 모델로 텍스트를 생성하려면 현재 대화에 텍스트 입력을 추가하고, 모델이 응답을 생성하게 요청하며, 모델 응답의 진행을 알려주는 서버 전송 이벤트를 들으면 돼요. 텍스트를 생성하려면 세션이 text modality로 구성돼야 해요(기본값). conversation.item.create 클라이언트 이벤트로 새 텍스트 대화 항목을 만들 수 있어요. REST API에서 Chat Completions에 사용자 메시지(프롬프트)를 보내는 것과 비슷해요.
event = {
"type": "conversation.item.create",
"item": {
"type": "message",
"role": "user",
"content": [
{
"type": "input_text",
"text": "What Prince album sold the most copies?",
}
],
},
}
ws.send(json.dumps(event))
사용자 메시지를 대화에 추가한 뒤 response.create 이벤트를 보내 모델 응답을 시작해요. 현재 세션에서 오디오와 텍스트가 모두 활성화돼 있으면 모델은 오디오와 텍스트를 둘 다 응답해요. 텍스트만 생성하려면 response.create 이벤트에서 지정할 수 있어요.
event = {"type": "response.create", "response": {"output_modalities": ["text"]}}
ws.send(json.dumps(event))
응답이 완전히 끝나면 서버가 response.done 이벤트를 내보내요. 이 이벤트에 모델이 생성한 전체 텍스트가 들어 있어요.
오디오 입력과 출력
음성 옵션
Realtime API는 오디오 출력에 사용할 수 있는 다양한 voice를 제공해요. 어떤 음성을 쓸지 세션 구성에서 선택할 수 있어요.
WebRTC로 오디오 다루기
WebRTC를 쓸 때는 대부분의 오디오 처리가 WebRTC API로 처리돼요. 마이크 입력을 받아 모델 오디오 출력을 재생하고, 서버는 session.created와 같은 이벤트를 통해 상태를 알려줘요.
WebSocket으로 오디오 다루기
WebSocket 연결에서는 오디오를 base64로 인코딩해 JSON 이벤트로 보내고 받아요. 오디오를 스트리밍 입력하려면 오디오를 청크로 나눠 input_audio_buffer.append 이벤트로 보내고, 입력이 끝나면 input_audio_buffer.commit을 보내요. 전체 오디오 메시지를 input_audio_buffer.append로 한 번에 보낼 수도 있어요. 서버의 오디오 출력은 response.audio.delta 이벤트로 도착하고, 재생하려면 이걸 누적해야 해요.
이미지 입력
gpt-realtime-2와 gpt-realtime은 이미지 입력도 지원해요. 사용자 메시지의 content part로 이미지를 붙일 수 있고, 모델이 응답할 때 이미지의 내용을 반영할 수 있어요.
const base64Image = "<a base64-encoded string of image bytes>";
const event = {
type: "conversation.item.create",
item: {
type: "message",
role: "user",
content: [
{
type: "input_image",
image_url: `data:image/{format};base64,${base64Image}`,
},
],
},
};
// WebRTC data channel and WebSocket both have .send()
dataChannel.send(JSON.stringify(event));
음성 활동 감지 (VAD)
기본적으로 Realtime 세션에는 voice activity detection (VAD) 이 켜져 있어요. 사용자가 말하기 시작하고 멈춘 시점을 API가 판단해 자동으로 응답해요. VAD를 상세히 구성하는 방법은 voice activity detection 가이드를 참고하세요.
VAD 비활성화하기
session.update 클라이언트 이벤트로 turn_detection을 null로 설정하면 VAD를 끌 수 있어요. 푸시투토크 인터페이스처럼 오디오 입력을 세밀하게 제어하고 싶은 인터페이스에 유용해요.
VAD를 끄면 클라이언트가 오디오 응답을 트리거하기 위해 몇 가지 클라이언트 이벤트를 수동으로 보내야 해요.
- 수동으로
input_audio_buffer.commit을 보내 대화에 새 사용자 입력 아이템을 만들어요. response.create를 수동으로 보내 모델의 오디오 응답을 트리거해요.- 새 사용자 입력을 시작하기 전에
input_audio_buffer.clear를 보내요.
VAD는 유지하되 자동 응답 끄기
VAD 모드를 유지하면서 응답 생성 시점을 직접 결정하고 싶다면, session.update 클라이언트 이벤트로 turn_detection.interrupt_response와 turn_detection.create_response를 false로 설정하면 돼요. VAD의 모든 동작은 유지되지만 새 Responses는 자동 생성되지 않아요. 클라이언트가 response.create 이벤트로 수동 트리거할 수 있어요. 입력에 대한 제어를 위해 상호작용 지연을 약간 감수해도 되는 중재(moderation)나 입력 검증, RAG 패턴에 유용해요.
기본 대화 밖에서 응답 생성하기
기본적으로 세션 중 생성된 모든 응답은 세션의 대화 상태("기본 대화")에 추가돼요. 하지만 세션의 기본 대화 컨텍스트 밖에서 모델 응답을 생성하거나, 여러 응답을 동시에 생성하고 싶을 수 있어요. 또 모델이 응답을 생성할 때 고려할 대화 항목을 더 세밀하게 제어하고 싶을 수도 있어요(예: 마지막 N턴만).
response.create 클라이언트 이벤트로 응답을 만들 때 response.conversation 필드를 문자열 none으로 설정하면, 기본 대화 상태에 추가되지 않는 "out-of-band" 응답을 생성할 수 있어요. Out-of-band 응답을 만들 때는 이 응답에 해당하는 서버 전송 이벤트를 식별할 방법이 필요할 텐데, 모델 응답에 metadata를 제공해서 어떤 클라이언트 이벤트에 대한 응답인지 식별할 수 있어요.
prompt = """
Analyze the conversation so far. If it is related to support, output
"support". If it is related to sales, output "sales".
"""
event = {
"type": "response.create",
"response": {
# Setting to "none" indicates the response is out of band,
# and will not be added to the default conversation
"conversation": "none",
# Set metadata to help identify responses sent back from the model
"metadata": {"topic": "classification"},
# Set any other available response fields
"output_modalities": ["text"],
"instructions": prompt,
},
}
ws.send(json.dumps(event))
이제 response.done 서버 이벤트를 들을 때 metadata를 확인해 out-of-band 응답의 결과를 식별할 수 있어요.
함수 호출
Realtime API는 함수 호출을 지원해요. 세션에서 session.update 이벤트로 호출 가능한 functions를 정의하고, 모델이 응답을 만들 때 커스텀 함수를 호출하고 싶어하면 response.create 이벤트 후 서버가 response.output에 type: "function_call"을 가진 항목을 내보내요.
서버가 내보내는 JSON에서 모델이 커스텀 함수를 호출하려는 것을 감지할 수 있어요.
| 속성 | 함수 호출 용도 |
|---|---|
response.output[0].type |
function_call이면 이 응답에 명명된 함수 호출용 인자가 들어 있음을 나타내요. |
response.output[0].name |
호출하도록 구성된 함수의 이름이에요. |
response.output[0].arguments |
함수에 대한 인자를 담은 JSON 문자열이에요. |
response.output[0].call_id |
이 함수 호출에 대한 시스템 생성 ID예요. 함수 호출 결과를 모델에 다시 전달하려면 이 ID가 필요해요. |
이 정보로 앱에서 함수를 실행(예: 외부 API 호출이나 데이터베이스 접근)해 결과를 구하고, 그 결과를 모델에 다시 제공할 수 있어요. conversation.item.create 클라이언트 이벤트로 결과를 담은 새 대화 항목을 만들어요.
{
"type": "conversation.item.create",
"item": {
"type": "function_call_output",
"call_id": "call_sHlR7iaFwQ2YQOqm",
"output": "{\"horoscope\": \"You will soon meet a new friend.\"}"
}
}
- 대화 항목 유형은
function_call_output이에요. item.call_id는 앞서response.done이벤트에서 받은 것과 같은 ID예요.item.output은 함수 호출 결과를 담은 JSON 문자열이에요.
함수 호출 결과를 담은 대화 항목을 추가한 뒤 클라이언트에서 다시 response.create 이벤트를 내보내면, 함수 호출 데이터를 사용한 모델 응답이 트리거돼요.
오류 처리
세션 중 서버에서 오류 조건이 발생하면 서버가 error 이벤트를 내보내요. 이런 오류는 앱이 내보낸 클라이언트 이벤트로 거슬러 올라갈 수 있어요. HTTP 요청·응답처럼 응답이 클라이언트 요청에 암묵적으로 묶이는 것과 달리, Realtime에서는 어떤 클라이언트 이벤트가 서버에서 오류 조건을 유발했는지 알려면 클라이언트 이벤트의 event_id 속성을 써야 해요. 아래 코드에서 클라이언트가 지원되지 않는 이벤트 유형을 내보내려 시도해요.
const event = {
event_id: "my_awesome_event",
type: "scooby.dooby.doo",
};
dataChannel.send(JSON.stringify(event));
이 실패한 이벤트는 다음과 같은 error 이벤트를 만들고, 응답에 event_id: "my_awesome_event"가 포함돼요.
{
"type": "invalid_request_error",
"code": "invalid_value",
"message": "Invalid value: 'scooby.dooby.doo' ...",
"param": "type",
"event_id": "my_awesome_event"
}
인터럽트와 잘라내기 (Truncation)
많은 음성 앱에서 사용자가 모델이 말하는 도중 인터럽트할 수 있어요. Realtime API는 VAD가 켜져 있을 때 사용자 음성을 감지해 진행 중인 응답을 취소하고 새 응답을 시작하는 방식으로 인터럽트를 처리해요. 이 시나리오에서 모델이 어디에서 인터럽트됐는지 알아야 자연스럽게 대화를 이어갈 수 있어요(예: 사용자가 "방금 마지막에 뭐라고 했죠?"라고 물을 때). 이를 모델의 마지막 응답을 잘라내는(truncating) 것, 즉 재생되지 않은 부분을 대화에서 제거한다고 불러요.
WebRTC와 SIP 연결에서는 서버가 출력 오디오 버퍼를 관리하므로 주어진 순간에 얼마나 재생됐는지 알아요. 사용자 인터럽트가 있을 때 서버는 재생되지 않은 오디오를 자동으로 잘라내요. WebSocket 연결에서는 클라이언트가 오디오 재생을 관리하므로 재생을 멈추고 잘라내기를 직접 처리해야 해요.
- 클라이언트가 서버의 새
input_audio_buffer.speech_started이벤트를 감시해요. 사용자가 말하기 시작했음을 나타내는 이벤트예요. 서버는 진행 중인 모델 응답을 자동으로 취소하고response.cancelled이벤트를 내보내요. - 클라이언트가 이 이벤트를 감지하면, 모델에서 재생 중이던 오디오 재생을 즉시 멈춰요. 인터럽트 전에 마지막 오디오 응답을 얼마나 재생했는지 기록해요.
- 클라이언트가
conversation.item.truncate이벤트를 보내 모델 마지막 응답의 재생되지 않은 부분을 대화에서 제거해요.
{
"type": "conversation.item.truncate",
"item_id": "item_1234", # this is the item ID of the model's last response
"content_index": 0,
"audio_end_ms": 1500 # truncate audio after 1.5 seconds
}
대본(transcript)도 함께 잘라낼까요? Realtime 모델은 대본과 오디오를 정밀하게 정렬할 만큼 정보가 충분하지 않아서, conversation.item.truncate는 주어진 위치에서 오디오를 자르고 재생되지 않은 부분의 텍스트 대본을 제거해요. 재생되지 않은 오디오 제거 문제는 해결하지만 잘린 대본은 제공하지 않아요.
Push-to-talk
Realtime API는 기본적으로 VAD를 사용해서 오디오 입력으로 모델 응답이 트리거돼요. VAD를 끄고 앱 수준 게이트로 오디오 입력을 언제 모델로 보낼지 제어하면 push-to-talk 상호작용도 할 수 있어요. 예를 들어 스페이스바를 누르고 있는 동안 오디오를 캡처하고, 떼면 응답을 트리거하는 식이에요. 일부 앱에서는 놀라울 만큼 잘 작동해요. 사용자가 상호작용을 제어할 수 있고, VAD 실패를 피하며, VAD 타임아웃을 기다리지 않기 때문에 반응이 빠르죠.
Push-to-talk 구현은 WebSockets와 WebRTC에서 조금 달라요. WebSocket 연결에서는 모든 이벤트가 같은 채널에 같은 순서로 보내지는 반면, WebRTC 연결은 오디오와 제어 이벤트용으로 별도 채널이 있어요.
WebSockets — VAD를 끄고, 누를 때 클라이언트에서 오디오 녹음을 시작하고(진행 중 응답이 있으면 response.cancel, 재생 중 출력이 있으면 재생을 멈추고 conversation.item.truncate 전송), 뗄 때 input_audio_buffer.append로 오디오를 보내고, input_audio_buffer.commit으로 입력을 확정하고, response.create로 응답을 트리거해요.
WebRTC와 SIP — 비슷하지만 입력 오디오 버퍼를 명시적으로 비워야 해요. 누를 때 input_audio_buffer.clear로 이전 입력을 비우고(진행 중 응답은 response.cancel, 재생 중 출력은 output_audio_buffer.clear로 비워 대화도 잘라냄), 뗄 때 input_audio_buffer.commit으로 확정하고 응답을 트리거해요.