关于 Realtime API 最令人兴奋的事情之一是,语音的情感、语气和节奏都传递给模型进行推理。传统的级联语音系统(涉及 STT 和 TTS)引入了一个中间转录步骤,依靠 SSML 或提示来近似韵律,这固有地会损失保真度。说话者的表现力实际上在翻译中丢失了。由于它可以处理原始音频,Realtime API 通过推理保留了这些音频属性,最大限度地减少了延迟,并使用音调和语调线索丰富了响应。因此,Realtime API 使由 LLM 驱动的语音翻译比以往任何时候都更接近于实时口译员。
本 cookbook 演示了如何使用 OpenAI 的 Realtime API,通过 WebSockets 构建多语言、单向翻译工作流程。它使用 speaker 应用程序中的 Realtime + WebSockets 集成 和 WebSocket 服务器来实现,以将翻译后的音频镜像到 listener 应用程序。
此演示的真实世界用例是多语言对话翻译,其中说话者对着 speaker 应用程序说话,听众通过 listener 应用程序以他们选择的母语听到翻译。想象一下一个会议室,一位演讲者用英语讲话,一位戴着耳机的参与者选择收听塔加拉语翻译。由于当前音频模型的基于回合的性质,说话者必须短暂暂停,以使模型能够处理和翻译语音。但是,随着模型变得更快更高效,这种延迟将大大减少,翻译将变得更加无缝。
让我们探索一下说明应用程序工作原理的主要功能和代码片段。如果您想在本地运行该应用程序,可以在随附的 repo 中找到代码。
该项目有两个应用程序 - speaker 应用程序和 listener 应用程序。speaker 应用程序从浏览器获取音频,fork 音频,并为每种语言创建一个唯一的 Realtime 会话,并通过 WebSocket 将其发送到 OpenAI Realtime API。翻译后的音频流回传,并通过单独的 WebSocket 服务器镜像到 listener 应用程序。listener 应用程序同时接收所有翻译后的音频流,但仅播放选定的语言。此架构专为 POC 设计,不适用于生产用例。让我们深入了解工作流程!
我们需要每种语言都有唯一的流 - 每种语言都需要唯一的提示以及与 Realtime API 的会话。我们在 translation_prompts.js
中定义这些提示。
Realtime API 由 GPT-4o Realtime 或 GPT-4o mini Realtime 提供支持,它们是基于回合的,并且针对对话语音用例进行了训练。为了确保模型返回翻译后的音频(即,我们想要直接翻译问题,而不是回答问题),我们希望使用提示中的少量示例来引导模型。如果您是出于特定原因或上下文进行翻译,或者有专门的词汇表可以帮助模型理解翻译的上下文,也请将其包含在提示中。如果您希望模型以特定的口音说话或以其他方式引导声音,您可以遵循我们的 cookbook 中关于 Steering Text-to-Speech for more dynamic audio generation 的技巧。
我们可以动态输入任何语言的语音。
// Define language codes and import their corresponding instructions from our prompt config file
const languageConfigs = [
{ code: 'fr', instructions: french_instructions },
{ code: 'es', instructions: spanish_instructions },
{ code: 'tl', instructions: tagalog_instructions },
{ code: 'en', instructions: english_instructions },
{ code: 'zh', instructions: mandarin_instructions },
];
我们需要处理连接到 Realtime API 的客户端实例的设置和管理,从而允许应用程序处理和流式传输不同语言的音频。clientRefs
保存了 RealtimeClient
实例的映射,每个实例都与一个语言代码(例如,法语的“fr”,西班牙语的“es”)相关联,代表与 Realtime API 的每个唯一客户端连接。
const clientRefs = useRef(
languageConfigs.reduce((acc, { code }) => {
acc[code] = new RealtimeClient({
apiKey: OPENAI_API_KEY,
dangerouslyAllowAPIKeyInBrowser: true,
});
return acc;
}, {} as Record<string, RealtimeClient>)
).current;
// Update languageConfigs to include client references
const updatedLanguageConfigs = languageConfigs.map(config => ({
...config,
clientRef: { current: clientRefs[config.code] }
}));
注意:dangerouslyAllowAPIKeyInBrowser
选项设置为 true,因为我们在浏览器中使用我们的 OpenAI API 密钥进行演示目的,但在生产环境中,您应该使用通过 OpenAI REST API 生成的 临时 API 密钥。
我们需要实际启动与 Realtime API 的连接,并将音频数据发送到服务器。当用户在 speaker 页面上单击“连接”时,我们启动该过程。
connectConversation
函数协调连接,确保所有必要的组件都已初始化并准备就绪以供使用。
const connectConversation = useCallback(async () => {
try {
setIsLoading(true);
const wavRecorder = wavRecorderRef.current;
await wavRecorder.begin();
await connectAndSetupClients();
setIsConnected(true);
} catch (error) {
console.error('Error connecting to conversation:', error);
} finally {
setIsLoading(false);
}
}, []);
connectAndSetupClients
确保我们正在使用正确的模型和声音。对于此演示,我们正在使用 gpt-4o-realtime-preview-2024-12-17 和 coral。
// Function to connect and set up all clients
const connectAndSetupClients = async () => {
for (const { clientRef } of updatedLanguageConfigs) {
const client = clientRef.current;
await client.realtime.connect({ model: DEFAULT_REALTIME_MODEL });
await client.updateSession({ voice: DEFAULT_REALTIME_VOICE });
}
};
使用 WebSockets 发送音频需要工作来管理入站和出站 PCM16 音频流(有关详细信息,请参阅此处)。我们使用 wavtools(一个用于在浏览器中录制和流式传输音频数据的库)来抽象化这一点。在这里,我们使用 WavRecorder
在浏览器中捕获音频。
此演示支持手动和语音活动检测 (VAD) 两种录制模式,说话者可以切换这些模式。为了获得更清晰的音频捕获,我们建议在此处使用手动模式。
const startRecording = async () => {
setIsRecording(true);
const wavRecorder = wavRecorderRef.current;
await wavRecorder.record((data) => {
// Send mic PCM to all clients
updatedLanguageConfigs.forEach(({ clientRef }) => {
clientRef.current.appendInputAudio(data.mono);
});
});
};
我们监听 response.audio_transcript.done
事件来更新音频的文字记录。这些输入文字记录由 Whisper 模型并行于 GPT-4o Realtime 推理生成,后者对原始音频进行翻译。
我们为每种可选择的语言同时运行一个 Realtime 会话,因此我们获得每种语言的文字记录(无论在 listener 应用程序中选择了哪种语言)。可以通过切换“显示文字记录”按钮来显示这些文字记录。
听众可以从翻译流的下拉菜单中选择,并在连接后动态更改语言。演示应用程序使用法语、西班牙语、塔加拉语、英语和普通话,但 OpenAI 支持 57 种以上的语言。
该应用程序连接到一个简单的 Socket.IO
服务器,该服务器充当音频数据的中继。当翻译后的音频从 Realtime API 流回时,我们将这些音频流镜像到 listener 页面,并允许用户选择语言并收听翻译后的流。
这里的关键函数是 connectServer
,它连接到服务器并设置音频流式传输。
// Function to connect to the server and set up audio streaming
const connectServer = useCallback(async () => {
if (socketRef.current) return;
try {
const socket = io('https://127.0.0.1:3001');
socketRef.current = socket;
await wavStreamPlayerRef.current.connect();
socket.on('connect', () => {
console.log('Listener connected:', socket.id);
setIsConnected(true);
});
socket.on('disconnect', () => {
console.log('Listener disconnected');
setIsConnected(false);
});
} catch (error) {
console.error('Error connecting to server:', error);
}
}, []);
这是一个演示,旨在提供灵感。我们在这里使用 WebSockets 以方便本地开发。但是,在生产环境中,我们建议使用 WebRTC(它在流式传输音频质量和更低延迟方面更好),并使用通过 OpenAI REST API 生成的 临时 API 密钥 连接到 Realtime API。
当前的 Realtime 模型是基于回合的 - 这最适合对话用例,而不是我们真正想要的单向流式传输用例的不间断的联合国式实时翻译。对于此演示,我们可以在模型返回翻译后的音频后立即从 speaker 应用程序捕获额外的音频(即,在 listener 应用程序播放翻译后的音频时捕获更多输入音频),但是我们可以一次捕获的音频长度是有限制的。说话者需要暂停以让翻译赶上。
总而言之,此 POC 演示了 Realtime API 的单向翻译使用,但是 fork 音频以供多种用途的想法可以扩展到翻译之外。其他工作流程可能是同时进行情感分析、实时护栏或生成字幕。