Realtime 번역을 사용하면 원본 오디오를 전용 번역 세션으로 스트리밍하고, 화자가 말하는 도중에도 번역된 오디오와 새로 생성된 텍스트를 받을 수 있습니다. 실시간 통역, 다국어 통화, 방송, 회의, 수업, 화상 대화방 등에 활용하세요.
사람이 하는 말을 번역하는 앱에는 gpt-realtime-translate 모델을 사용하세요. 질문에 답하고 도구를 호출하며 대화를 이끄는 어시스턴트가 필요하다면 표준 Realtime 세션에서 gpt-realtime-2.1 모델을 사용하세요.
Realtime 번역 세션과 음성 에이전트 세션은 동작 방식이 다릅니다.
| 음성 에이전트 세션 |
번역 세션 |
/v1/realtime에 연결합니다. |
/v1/realtime/translations에 연결합니다. |
| 모델이 어시스턴트 역할을 합니다. |
모델이 통역사 역할을 합니다. |
| 대화와 응답의 수명 주기에 따라 동작합니다. |
들어오는 오디오를 바탕으로 출력을 계속 스트리밍합니다. |
| 도구를 호출하고 어시스턴트 차례에 응답을 생성할 수 있습니다. |
번역된 오디오와 텍스트를 생성되는 대로 출력합니다. |
response.create를 호출할 수 있습니다. |
response.create를 호출하지 않습니다. |
오디오 스트림이 들어오면 번역이 시작됩니다. 구절 사이의 무음까지 포함해 오디오를 계속 추가하고, 출력 이벤트가 도착할 때마다 처리하세요.
브라우저에서 오디오를 캡처하거나 재생할 때는 WebRTC를 사용하세요. WebRTC는 원본 오디오를 미디어 트랙으로 보내고 번역된 음성을 원격 오디오 트랙으로 받습니다. 따라서 PCM 청크를 직접 리샘플링하거나 재생할 필요가 없습니다.
서버가 Twilio Media Streams, SIP 미디어, 방송 입력 스트림, 미디어 워커 등에서 이미 원시 오디오를 받고 있다면 WebSocket을 사용하세요. WebSocket에서는 base64로 인코딩한 24 kHz PCM16 오디오를 보내고 반환되는 오디오 조각을 직접 재생하세요.
브라우저 앱의 경우 서버에서 유효 기간이 짧은 클라이언트 시크릿을 생성하세요. 일반 API 키를 브라우저에 노출하지 마세요.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25app.post("/session", async (req, res) => {
const language = req.body.targetLanguage ?? "es";
const response = await fetch(
"https://api.openai.com/v1/realtime/translations/client_secrets",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
"Content-Type": "application/json",
"OpenAI-Safety-Identifier": "hashed-user-id",
},
body: JSON.stringify({
session: {
model: "gpt-realtime-translate",
audio: {
output: { language },
},
},
}),
}
);
res.status(response.status).json(await response.json());
});
브라우저에서 오디오를 캡처하고 피어 연결을 만든 다음, SDP 오퍼를 번역 통화 엔드포인트에 POST 요청으로 보내세요.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50const { value: clientSecret } = await fetch("/session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ targetLanguage: "es" }),
}).then((response) => response.json());
const sourceStream = await navigator.mediaDevices.getUserMedia({
audio: true,
});
const pc = new RTCPeerConnection();
pc.addTrack(sourceStream.getAudioTracks()[0], sourceStream);
const translatedAudio = new Audio();
translatedAudio.autoplay = true;
pc.ontrack = ({ streams }) => {
translatedAudio.srcObject = streams[0];
};
const events = pc.createDataChannel("oai-events");
events.onmessage = ({ data }) => {
const event = JSON.parse(data);
if (event.type === "session.output_transcript.delta") {
subtitles.textContent += event.delta;
}
};
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
const sdpResponse = await fetch(
"https://api.openai.com/v1/realtime/translations/calls",
{
method: "POST",
headers: {
Authorization: `Bearer ${clientSecret}`,
"Content-Type": "application/sdp",
},
body: offer.sdp,
}
);
if (!sdpResponse.ok) {
throw new Error(await sdpResponse.text());
}
await pc.setRemoteDescription({
type: "answer",
sdp: await sdpResponse.text(),
});
번역 전용 엔드포인트에 연결할 때 URL에서 모델을 지정하세요.
예제를 실행하기 전에 Node.js에서는 ws를, Python에서는 websocket-client와 sounddevice를, Ruby에서는 async-websocket을 설치하세요(gem install async-websocket). Java에서는 Maven 종속성 com.openai:openai-java:4.75.1을 추가하세요.
Java 예제는 연결 설정, 언어 설정, 오디오 입력을 각각 따로 다룹니다. Java SDK는 이벤트 수신과 세션 종료도 지원하지만, 이 가이드의 Java 예제에는 해당 단계가 포함되어 있지 않습니다. 아래의 전체 번역 워크플로우를 따라 하려면 Node.js 또는 Python 예제를 사용하세요.
1
2
3
4
5
6
7
8
9
10
11import WebSocket from "ws";
const ws = new WebSocket(
"wss://api.openai.com/v1/realtime/translations?model=gpt-realtime-translate",
{
headers: {
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
"OpenAI-Safety-Identifier": "hashed-user-id",
},
}
);
1
2
3
4
5
6
7
8
9
10
11import os
import websocket
ws = websocket.WebSocket()
ws.connect(
"wss://api.openai.com/v1/realtime/translations?model=gpt-realtime-translate",
header=[
f"Authorization: Bearer {os.environ['OPENAI_API_KEY']}",
"OpenAI-Safety-Identifier: hashed-user-id",
],
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
client := openai.NewClient()
session, err := client.Realtime.ConnectTranslation(ctx, "gpt-realtime-translate", realtime.TranslationConnectionOptions{}, option.WithHeader("OpenAI-Safety-Identifier", "hashed-user-id"))
if err != nil {
return err
}
defer session.Abort()
created, err := session.Recv(ctx)
if err != nil {
return err
}
fmt.Println(created.Type)
if created.Type != "session.created" {
return fmt.Errorf("expected session.created, got %s", created.Type)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32import com.openai.client.okhttp.OkHttpClient;
import com.openai.core.ClientOptions;
import com.openai.helpers.TranslationConnection;
import com.openai.helpers.TranslationWebSocketOptions;
import com.openai.models.realtime.*;
var http = OkHttpClient.builder().build();
var options =
ClientOptions.builder()
.fromEnv()
.httpClient(http)
.putHeader("OpenAI-Safety-Identifier", "hashed-user-id")
.build();
try (http;
var connection =
TranslationConnection.connect(
options,
TranslationWebSocketOptions.builder()
.model("gpt-realtime-translate")
.sendTimeout(java.time.Duration.ofSeconds(30))
.build())) {
while (true) {
var event = connection.receive();
if (event.error().isPresent())
throw new IllegalStateException(event.error().orElseThrow().toString());
if (event.sessionCreated().isPresent()) {
System.out.println("Connected to translation session.");
System.out.println(event);
break;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21require "async"
require "async/http/endpoint"
require "async/websocket/client"
require "json"
endpoint = Async::HTTP::Endpoint.parse("wss://api.openai.com/v1/realtime/translations?model=gpt-realtime-translate", timeout: 10, alpn_protocols: ["http/1.1"])
headers = {
"Authorization" => "Bearer #{ENV.fetch("OPENAI_API_KEY")}",
"OpenAI-Safety-Identifier" => "hashed-user-id"
}
Sync do |task|
task.with_timeout(120) do
Async::WebSocket::Client.connect(endpoint, headers: headers) do |connection|
message = connection.read or raise "Connection closed before session creation"
event = JSON.parse(message.to_str)
raise "Expected session.created: #{event}" unless event["type"] == "session.created"
puts(event.fetch("type"))
end
end
end
Ruby에서는 아래의 언어 설정 및 오디오 추가 코드를 Async::WebSocket::Client.connect 블록 안에 넣으세요. 세션 생성 여부를 확인하는 코드 뒤, 블록이 끝나기 전에 넣으면 됩니다. 오디오를 보내고 번역 이벤트를 받는 동안 연결을 유지하세요.
Java 연결 예제에서는 try 블록이 끝나면 소켓이 닫힙니다. Java 언어 설정 및 오디오 입력 예제를 실행하려면 TranslationConnection이 열려 있어야 합니다. 오디오를 보내고 출력을 받는 동안 연결을 유지하세요. 블록을 벗어나기 전에 세션을 종료하고 session.closed까지 남은 이벤트를 모두 읽으세요. 자세한 절차는 WebSocket 세션 닫기를 참고하세요.
소켓이 열리면 대상 언어를 설정하세요.
1
2
3
4
5
6
7
8
9
10
11
12
13
14ws.on("open", () => {
ws.send(
JSON.stringify({
type: "session.update",
session: {
audio: {
output: {
language: "es",
},
},
},
})
);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16import json
ws.send(
json.dumps(
{
"type": "session.update",
"session": {
"audio": {
"output": {
"language": "es",
},
},
},
}
)
)
1
2
3
4
5
6
7
8
9if err := session.Send(ctx, realtime.RealtimeTranslationClientEventParamOfSessionUpdate(realtime.RealtimeTranslationSessionUpdateRequestParam{
Audio: realtime.RealtimeTranslationSessionUpdateRequestAudioParam{
Output: realtime.RealtimeTranslationSessionUpdateRequestAudioOutputParam{
Language: openai.String("es"),
},
},
})); err != nil {
return err
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20import com.openai.client.okhttp.OkHttpClient;
import com.openai.core.ClientOptions;
import com.openai.helpers.TranslationConnection;
import com.openai.helpers.TranslationWebSocketOptions;
import com.openai.models.realtime.*;
connection.send(
RealtimeTranslationClientEvent.ofSessionUpdate(
RealtimeTranslationSessionUpdateEvent.builder()
.session(
RealtimeTranslationSessionUpdateRequest.builder()
.audio(
RealtimeTranslationSessionUpdateRequest.Audio.builder()
.output(
RealtimeTranslationSessionUpdateRequest.Audio.Output.builder()
.language("es")
.build())
.build())
.build())
.build()));
1
2connection.write(JSON.generate(type: "session.update", session: { audio: { output: { language: "es" } } }))
connection.flush
그런 다음 오디오를 계속 추가하세요.
1
2
3
4
5
6ws.send(
JSON.stringify({
type: "session.input_audio_buffer.append",
audio: base64Pcm16,
})
);
1
2
3
4
5
6
7
8ws.send(
json.dumps(
{
"type": "session.input_audio_buffer.append",
"audio": base64_pcm16,
}
)
)
1
2
3
4
5
6
7// audio contains mono PCM16 samples at 24 kHz.
for start := 0; start < len(audio); start += 4800 {
end := min(start+4800, len(audio))
if err := session.Send(ctx, realtime.RealtimeTranslationClientEventParamOfSessionInputAudioBufferAppend(base64.StdEncoding.EncodeToString(audio[start:end]))); err != nil {
return err
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14import com.openai.client.okhttp.OkHttpClient;
import com.openai.core.ClientOptions;
import com.openai.helpers.TranslationConnection;
import com.openai.helpers.TranslationWebSocketOptions;
import com.openai.models.realtime.*;
import java.util.Base64;
// Call for each PCM16, 24 kHz, mono chunk supplied by your audio source.
static void appendAudio(TranslationConnection connection, byte[] pcmAudio) {
String base64Pcm16 = Base64.getEncoder().encodeToString(pcmAudio);
connection.send(
RealtimeTranslationClientEvent.ofSessionInputAudioBufferAppend(
RealtimeTranslationInputAudioBufferAppendEvent.builder().audio(base64Pcm16).build()));
}
1
2
3
4
5
6
7
8require "base64"
File.open("speech.pcm", "rb") do |audio|
while (chunk = audio.read(4_800))
connection.write(JSON.generate(type: "session.input_audio_buffer.append", audio: Base64.strict_encode64(chunk)))
connection.flush
end
end
번역된 오디오와 텍스트를 수신하세요.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15ws.on("message", (data) => {
const event = JSON.parse(data.toString());
if (event.type === "session.output_audio.delta") {
playPcm16(event.delta);
}
if (event.type === "session.output_transcript.delta") {
process.stdout.write(event.delta);
}
if (event.type === "session.input_transcript.delta") {
updateSourceTranscript(event.delta);
}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50import base64
import json
from concurrent.futures import ThreadPoolExecutor
from contextlib import suppress
from queue import SimpleQueue
import sounddevice as sd
audio_queue = SimpleQueue()
def play_audio():
try:
# Closing the stream waits for PortAudio's pending buffers to finish.
with sd.RawOutputStream(samplerate=24000, channels=1, dtype="int16") as stream:
while (pcm := audio_queue.get()) is not None:
stream.write(pcm)
except Exception:
# The receiver may have already closed the socket.
with suppress(OSError):
ws.abort() # Wake recv() so the receiver can report the playback failure.
raise
with ThreadPoolExecutor(max_workers=1) as executor:
playback = executor.submit(play_audio)
try:
while True:
event = json.loads(ws.recv())
if playback.done():
playback.result() # Report a playback failure on the receiver.
if event["type"] == "session.output_audio.delta":
audio_queue.put(base64.b64decode(event["delta"]))
if event["type"] in (
"session.output_transcript.delta",
"session.input_transcript.delta",
):
print(event["delta"], end="", flush=True)
if event["type"] == "error":
raise RuntimeError(f"Translation error: {event['error']['code']}")
if event["type"] == "session.closed":
break
finally:
ws.close()
audio_queue.put(None)
playback.result()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24// Run this listener while another goroutine sends audio into the session.
func listen(ctx context.Context, session *realtime.TranslationSession) error {
for {
event, err := session.Recv(ctx)
if err != nil {
return err
}
switch event.Type {
case "error":
return fmt.Errorf("translation error: %s", event.OfRealtimeErrorEvent.Error.Message)
case "session.output_transcript.delta":
fmt.Print(event.OfRealtimeTranslationOutputTranscriptDeltaEvent.Delta)
case "session.output_audio.delta":
pcm, err := base64.StdEncoding.DecodeString(event.OfRealtimeTranslationOutputAudioDeltaEvent.Delta)
if err != nil {
return err
}
// Feed pcm into your application's 24 kHz audio player.
fmt.Fprintln(log.Writer(), "Received audio bytes:", len(pcm))
case "session.closed":
return session.Close()
}
}
}
Go에서는 오디오를 보내는 동안 listen을 동시에 실행하세요. 이 함수는 session.closed를 받으면 연결을 닫습니다.
원본 스트림이 끝나면 WebSocket을 닫기 전에 session.close 이벤트를 보내세요. 이 이벤트를 받으면 서비스는 대기 중인 입력 오디오를 모두 처리하고, 남은 번역 오디오와 텍스트를 출력한 다음 session.closed 이벤트를 보냅니다. session.close 이벤트는 번역 세션에서만 지원됩니다.
session.close를 보낸 후에는 오디오 추가를 중단하고, 기존 수신 루프에서 session.closed를 받을 때까지 이벤트를 계속 읽으세요. 소켓을 바로 닫으면 세션에서 아직 전송 중인 번역 결과가 누락될 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59let translationSessionClosing = false;
let gotTranslationClosed = false;
let drainDeadline;
ws.on("close", () => {
clearTimeout(drainDeadline);
if (!gotTranslationClosed) {
console.error("Translation disconnected before final session.closed");
process.exitCode = 1;
}
});
function closeTranslationSession() {
if (translationSessionClosing) {
return;
}
translationSessionClosing = true;
// Adjust this deadline for your application's finalization requirements.
drainDeadline = setTimeout(() => {
console.error("Translation finalization timed out");
process.exitCode = 1;
ws.close();
}, 15_000);
ws.send(JSON.stringify({ type: "session.close" }));
}
// Use this in place of the earlier output handler, so audio plays only once.
ws.on("message", (data) => {
const event = JSON.parse(data.toString());
if (event.type === "session.output_audio.delta") {
playPcm16(event.delta);
}
if (event.type === "session.output_transcript.delta") {
process.stdout.write(event.delta);
}
if (event.type === "session.input_transcript.delta") {
updateSourceTranscript(event.delta);
}
if (event.type === "error") {
clearTimeout(drainDeadline);
console.error("Translation error", event.error?.code);
process.exitCode = 1;
ws.close();
}
if (event.type === "session.closed") {
clearTimeout(drainDeadline);
gotTranslationClosed = true;
ws.close();
}
});
// Call this when the source stream ends.
closeTranslationSession();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76import base64
import json
import time
from concurrent.futures import ThreadPoolExecutor
from contextlib import suppress
from queue import SimpleQueue
import sounddevice as sd
audio_queue = SimpleQueue()
def play_audio():
try:
# Closing the stream waits for PortAudio's pending buffers to finish.
with sd.RawOutputStream(samplerate=24000, channels=1, dtype="int16") as stream:
while (pcm := audio_queue.get()) is not None:
stream.write(pcm)
except Exception:
# The receiver may have already closed the socket.
with suppress(OSError):
ws.abort() # Wake recv() so the receiver can report the playback failure.
raise
translation_session_closing = False
def close_translation_session():
global translation_session_closing
if translation_session_closing:
return
translation_session_closing = True
ws.send(json.dumps({"type": "session.close"}))
with ThreadPoolExecutor(max_workers=1) as executor:
playback = executor.submit(play_audio)
try:
# Call this when the source stream ends.
close_translation_session()
# Adjust this deadline for your application's finalization requirements.
drain_deadline = time.monotonic() + 15.0
while True:
remaining = drain_deadline - time.monotonic()
if remaining <= 0:
raise TimeoutError("Translation finalization timed out")
ws.settimeout(remaining)
data = ws.recv()
if not data:
raise RuntimeError("Translation disconnected before session.closed")
event = json.loads(data)
if playback.done():
playback.result() # Report a playback failure on the receiver.
if event["type"] == "error":
raise RuntimeError(f"Translation error: {event['error']['code']}")
if event["type"] == "session.output_audio.delta":
audio_queue.put(base64.b64decode(event["delta"]))
if event["type"] in (
"session.output_transcript.delta",
"session.input_transcript.delta",
):
print(event["delta"], end="", flush=True)
if event["type"] == "session.closed":
break
finally:
# The server deadline covers receiving events, not local playback.
ws.close()
audio_queue.put(None)
playback.result()
1
2
3
4
5
6// Stop sending audio first. The listener closes the transport after session.closed.
if err := session.Send(ctx, realtime.RealtimeTranslationClientEventUnionParam{
OfSessionClose: &realtime.RealtimeTranslationSessionCloseEventParam{},
}); err != nil {
return err
}
한 사람의 발언이나 하나의 스트림을 번역해 청중에게 들려줄 때 이 방식을 사용하세요. 라이브 스트리밍, 콘퍼런스 발표, 웨비나, 실적 발표 컨퍼런스콜, 강의, 동영상 등이 대표적인 예입니다.
일반적인 구조는 다음과 같습니다.
source audio -> translation session -> translated audio + subtitles
대상 언어마다 번역 세션을 하나씩 만드세요. 같은 영어 원본을 스페인어와 프랑스어로 출력해야 한다면 영어를 스페인어로 번역하는 세션과 영어를 프랑스어로 번역하는 세션을 각각 만드세요.
브라우저에서 통역을 들려주는 앱에서는 getDisplayMedia()로 탭 오디오를 캡처해 WebRTC로 보내고, 번역된 원격 오디오 트랙을 재생하세요. 실제 방송 환경에서는 서버의 미디어 워커에서 번역을 실행하고 번역된 오디오 트랙이나 자막을 청중에게 송출하세요.
두 명 이상의 참가자가 서로 다른 언어로 대화할 때는 대화 통역을 사용하세요. 고객 지원 통화, 영업 통화, 개인 지도, 화상 대화방 등에 활용할 수 있습니다.
참가자별 오디오 트랙을 분리하세요. 여러 화자의 음성을 하나의 스트림으로 합치면 화자를 구분하고 화자별 자막을 표시하거나 겹치는 발화를 처리하기가 어려워집니다.
두 사람이 통화할 때는 번역 방향별로 세션을 하나씩 만드세요.
Caller A audio -> translate into Caller B language -> play to Caller B
Caller B audio -> translate into Caller A language -> play to Caller A
그룹 대화방에서 필요한 세션 수는 현재 발언 중인 화자 수와 대상 언어 수에 따라 달라집니다.
translation sessions ~= active source speaker tracks x distinct target languages
소규모 대화방에서는 각 청취자가 통역을 원하는 원격 화자별로 브라우저에 번역 사이드카를 만들 수 있습니다. 대규모 대화방에서는 서버 측 참가자나 미디어 워커를 사용하세요. 이 참가자나 워커가 각 화자의 원본 트랙을 한 번씩 구독하고, 대상 언어마다 번역 세션을 하나씩 만든 뒤 번역된 트랙을 다시 배포합니다.
실제 오디오로 번역을 테스트하고 두 언어에 능통한 검토자에게 결과를 확인받으세요. 자동 평가 지표도 도움이 되지만, 사용자가 알아차리는 오류를 모두 찾아내지는 못합니다.
다음 항목을 테스트하세요.
- 언어 쌍별 번역 품질
- 이름, 숫자, 날짜, 통화, 전화번호의 번역 정확도
- 전문 분야의 용어
- 발화 중 언어 전환과 여러 언어가 섞인 대화
- 다양한 억양, 빠른 말, 겹치는 발화
- 첫 번역 오디오가 출력될 때까지의 지연 시간
- 발화 종료 시점의 지연 시간
- 자막 표시 타이밍
- 음성의 일관성
- 재연결 시 동작
이름이나 전문 용어를 정확하게 번역해야 한다면 출시 전에 정답이 있는 평가 데이터 세트를 만들고 실패 사례를 직접 검토하세요.
- 브라우저 미디어에는 WebRTC를, 서버 미디어에는 WebSocket을 사용하세요.
- 번역 전용
/v1/realtime/translations 엔드포인트를 사용하세요.
- 구절 사이의 무음까지 포함해 오디오를 끊김 없이 스트리밍하세요.
- WebSocket 세션을 닫기 전에
session.close를 보내고 session.closed가 도착할 때까지 기다리세요.
- 대화 통역에서는 화자별 트랙을 분리하세요.
- 출력 언어마다 세션을 하나씩 사용하세요.
- 필요에 따라 원문과 번역문을 함께 표시하세요.
- 사용자가 원본 오디오, 번역 오디오, 자막, 음소거, 음량을 제어할 수 있도록 하세요.
- 재연결 중, 지연, 이용 불가 상태를 사용자에게 표시하세요.
- 번역 품질과 별도로 지연 시간을 추적하세요.
음성 에이전트, 번역, 음성 전사 세션을 비교하세요.
브라우저 미디어를 Realtime 세션에 연결하세요.
서버 측 미디어 파이프라인으로 원시 오디오를 스트리밍하세요.
실시간 오디오를 텍스트로 변환하고 새로 생성된 텍스트를 스트리밍하세요.