narrow-chef-71655
12/18/2025, 10:37 AMtall-airline-16756
12/18/2025, 10:37 AMtall-airline-16756
12/18/2025, 10:38 AMnarrow-chef-71655
12/18/2025, 10:38 AMtall-airline-16756
12/18/2025, 10:39 AMopenai.realtime.RealtimeModel. This example shows the minimal edits to your existing script to run a realtime translator that listens and speaks translations.
1) Key edits
• Replace separate stt, llm, and tts with a single openai.realtime.RealtimeModel.
• Provide translator instructions via instructions.
• Keep VAD for responsiveness (optional).
• Optionally set modalities=[ "text" ] if you want the model to return text only and use a separate TTS.
2) Minimal example (drop into your current example, adjust imports/agent class as needed):
from dotenv import load_dotenv
from livekit.agents import AgentSession, cli, JobContext
from livekit.plugins import openai, silero
load_dotenv()
class RealTimeTranslatorAgent:
async def on_enter(self, session: AgentSession):
await session.say("Starting realtime translator.", language="en")
@cli.multiprocess_worker
async def entrypoint(ctx: JobContext):
# Configure OpenAI RealtimeModel
llm = openai.realtime.RealtimeModel(
voice="alloy", # model voice for audio output
modalities=["audio", "text"], # enable audio I/O (use ["text"] for text-only responses)
instructions=(
"You are a live translator. Listen to the user speaking in any language and "
"translate their meaning into French unless they request another language."
),
turn_detection=openai.realtime.TurnDetection(
type="server_vad",
threshold=0.5,
prefix_padding_ms=300,
silence_duration_ms=800,
),
)
session = AgentSession(
llm=llm,
vad=silero.VAD.load()
)
await session.start(agent=RealTimeTranslatorAgent(), room=ctx.room)
await ctx.connect()
3) Optional hybrid modes
• If you want the model to only output text (so you can use a custom TTS), set modalities=["text"] and add your tts= plugin to AgentSession.
• If you need explicit transcripts for logging or language detection, keep a fast STT and set the realtime model to text-only, then feed transcripts to the model.
4) Env and deps
• Ensure env vars: OPENAI_API_KEY, LIVEKIT_* keys.
• Install: pip install livekit-agents[openai,silero] python-dotenv
Would you like:
• a full patched diff against the exact file line you linked, or
• an example that outputs text-only and uses ElevenLabs for French TTS? Sources: Realtime model plugins | LiveKit Docs | OpenAI and LiveKit partner to turn Advanced Voice into an API | OpenAI Realtime API plugin guide | LiveKit docsnarrow-chef-71655
12/18/2025, 1:09 PMclass Translator:
"""Handles real-time translation of transcribed text to target languages."""
def __init__(
self,
*,
room: rtc.Room,
track_id: str,
target_language: Language,
participant_identity: str,
):
"""Initialize translator for a specific language."""
self._room = room
self._target_language = target_language
self._track_id = track_id # source track id
self._participant_identity = participant_identity
self._tasks: list[asyncio.Task] = []
self._input_ch = utils.aio.Chan[str]()
self._playout_ch = utils.aio.Chan[PlayoutData]()
self._llm_stream = utils.aio.Chan[str]()
self._llm = google.LLM()
self._tts = elevenlabs.TTS()
self._resampler = rtc.AudioResampler(22050, 48000)
def start(self):
self._tasks.append(asyncio.create_task(self._run()))
self._tasks.append(asyncio.create_task(self._synthesize_tts()))
self._tasks.append(asyncio.create_task(self._publish_and_playout()))
async def aclose(self):
self.end_input()
await asyncio.gather(*self._tasks)
self._tasks.clear()
def push_sentence(self, sentence: str):
self._input_ch.send_nowait(sentence)
def end_input(self):
if not self._input_ch.closed:
self._input_ch.close()
@utils.log_exceptions(logger=logger)
async def _run(self):
"""Translate sentences to target language and enqueue the translated audio frames."""
async for sentence in self._input_ch:
if not sentence:
continue
context = llm.ChatContext()
context.add_message(
role="system",
content=f"You are a translator for language: {self._target_language.name}. "
f"Your only response should be the exact translation of input text in the {self._target_language.name} language.",
)
context.add_message(role="user", content=sentence)
try:
llm_stream = self._llm.chat(chat_ctx=context)
response = ""
async for chunk in llm_stream:
if not chunk.delta or not chunk.delta.content:
continue
response += chunk.delta.content
await llm_stream.aclose()
<http://logger.info|logger.info>(f"translated to {self._target_language.name}: {response}")
self._llm_stream.send_nowait(response)
except Exception:
logger.exception("Error translating sentence")
self._llm_stream.close()
@utils.log_exceptions(logger=logger)
async def _synthesize_tts(self):
async for sentence in self._llm_stream:
stream = self._tts.synthesize(sentence)
playout_data = PlayoutData(sentence)
self._playout_ch.send_nowait(playout_data)
async for frame in stream:
frames = self._resampler.push(frame.frame)
for f in frames:
playout_data.audio_ch.send_nowait(f)
playout_data.audio_ch.close()
await stream.aclose()
<http://logger.info|logger.info>("ending tts input")
self._playout_ch.close()
@utils.log_exceptions(logger=logger)
async def _publish_and_playout(self):
"""Publish the translated audio frames to the room and play them out."""
audio_output = voice._ParticipantAudioOutput(
self._room,
sample_rate=48000,
num_channels=1,
track_publish_options=rtc.TrackPublishOptions(),
track_name=f"{self._track_id}-{self._target_language.code}",
)
await audio_output.start()
text_output = voice._ParticipantStreamTranscriptionOutput(
self._room,
participant=self._participant_identity,
is_delta_stream=True,
attributes={
"language": self._target_language.code,
"translated": "true",
},
)
synchronizer = voice.TranscriptSynchronizer(
next_in_chain_audio=audio_output,
next_in_chain_text=text_output,
)
async for playout_data in self._playout_ch:
<http://logger.info|logger.info>(f"translated playing out: {playout_data.transcript}")
await synchronizer.text_output.capture_text(playout_data.transcript)
synchronizer.text_output.flush()
async for frame in playout_data.audio_ch:
await synchronizer.audio_output.capture_frame(frame)
synchronizer.audio_output.flush()
await synchronizer.aclose()
await audio_output.aclose()
class InputTrack:
def __init__(
self,
*,
language: str,
track: rtc.RemoteAudioTrack,
participant_identity: str,
room: rtc.Room,
):
self.language = language
self.track = track
self.participant_identity = participant_identity
self.room = room
self._translators: dict[str, Translator] = {}
self._tasks: list[asyncio.Task] = []
self._stt = deepgram.STT(language=language, model="nova-2")
tokenizer = tokenize.blingfire.SentenceTokenizer()
self._sentence_stream: tokenize.SentenceStream = tokenizer.stream()
self._stt_stream = self._stt.stream(language=language)
def start(self):
self._tasks.append(asyncio.create_task(self._consume_input()))
self._tasks.append(asyncio.create_task(self._tokenize_to_sentences()))
self._tasks.append(asyncio.create_task(self._forward_to_translators()))
async def aclose(self):
await utils.aio.cancel_and_wait(*self._tasks)
self._tasks.clear()
for translator in self._translators.values():
await translator.aclose()
def set_languages(self, target_languages: list[str], *, room: rtc.Room):
for lang in target_languages:
if lang != self.language:
self._add_translator(lang, room)
for lang in list(self._translators.keys()):
if lang != self.language and lang not in target_languages:
self._remove_translator(lang)
def _add_translator(self, target_language: str, room: rtc.Room):
lang = language_map.get(target_language)
if not lang:
return
if lang.code in self._translators:
return
<http://logger.info|logger.info>(f"starting translator for {self.track.sid} - {lang.name}")
translator = Translator(
room=room,
target_language=lang,
track_id=self.track.sid,
participant_identity=self.participant_identity,
)
translator.start()
self._translators[lang.code] = translator
def _remove_translator(self, target_language: str):
translator = self._translators.pop(target_language, None)
if translator:
asyncio.create_task(translator.aclose())
async def _forward_to_translators(self):
"""Forward transcribed sentences to each language specific translators."""
async for ev in self._sentence_stream:
<http://logger.info|logger.info>(f"forwarding to translators: {ev.token}")
for translator in self._translators.values():
translator.push_sentence(ev.token)
<http://logger.info|logger.info>("ending translator input")
for translator in self._translators.values():
translator.end_input()
@utils.log_exceptions(logger=logger)
async def _tokenize_to_sentences(self):
"""tokenize STT output to sentences."""
async def _create_text_writer(*, segment_id: str) -> rtc.TextStreamWriter:
attributes = {
ATTRIBUTE_TRANSCRIPTION_FINAL: "false",
ATTRIBUTE_TRANSCRIPTION_TRACK_ID: self.track.sid,
ATTRIBUTE_TRANSCRIPTION_SEGMENT_ID: segment_id,
"language": self.language,
}
return await self.room.local_participant.stream_text(
topic=TOPIC_TRANSCRIPTION,
sender_identity=self.participant_identity,
attributes=attributes,
)
segment_id = utils.shortuuid("SG_")
writer: rtc.TextStreamWriter | None = None
async for ev in self._stt_stream:
if ev.type == stt.SpeechEventType.FINAL_TRANSCRIPT:
<http://logger.info|logger.info>(f"STT: {ev.alternatives[0].text}")
self._sentence_stream.push_text(ev.alternatives[0].text)
if writer is None:
writer = await _create_text_writer(segment_id=segment_id)
await writer.write(ev.alternatives[0].text)
elif ev.type == stt.SpeechEventType.END_OF_SPEECH:
self._sentence_stream.flush()
if writer:
attributes = {
ATTRIBUTE_TRANSCRIPTION_FINAL: "true",
ATTRIBUTE_TRANSCRIPTION_TRACK_ID: self.track.sid,
}
await writer.aclose(attributes=attributes)
writer = None
segment_id = utils.shortuuid("SG_")
self._sentence_stream.end_input()
async def _consume_input(self):
"""Transcribe the audio stream and run through STT."""
stream = rtc.AudioStream.from_track(track=self.track)
async for ev in stream:
self._stt_stream.push_frame(ev.frame)
await stream.aclose()tall-airline-16756
12/18/2025, 1:10 PMrtc.AudioFrame from LiveKit AudioStream
• raw PCM bytes / numpy arrays (specify sample rate & channels)
3) How you want outputs delivered (pick one):
• async callback functions for audio/text
• asyncio.Queue/Chan you will consume
• async generator you can iterate
Answer those and I’ll send a compact Python class implementing only the Realtime LLM handling (no AgentSession, no room logic), ready to plug into your existing pipeline.
Sources: OpenAI Realtime API plugin guide | LiveKit docs | Azure OpenAI Realtime API and LiveKit | LiveKit docs | OpenAI and LiveKit partner to turn Advanced Voice into an API | Agents (Node.js) | livekit/livekit_composite | DeepWikinarrow-chef-71655
12/18/2025, 1:14 PMtall-airline-16756
12/18/2025, 1:14 PMrtc.AudioFrame input and async callbacks for translated audio output. This class avoids AgentSession and room logic; it only manages the RealtimeModel/session, pushes audio frames in, and invokes your async callback with output frames.
Important: the LiveKit examples expose RealtimeModel and session concepts but don’t show every low-level method. The snippet below follows the same patterns used by LiveKit’s RealtimeModel usage and provides the minimal, practical API you requested. Integrate it into your pipeline by calling start(), repeatedly await push_frame(frame), and handling audio via on_audio callback.
import asyncio
import logging
from typing import Callable, Awaitable, Optional
from livekit.plugins import openai
from livekit import rtc
logger = logging.getLogger("realtime_translator")
class RealtimeLLMTranslator:
"""
Audio-in -> Audio-out translator using OpenAI RealtimeModel.
Usage:
translator = RealtimeLLMTranslator(
target_language="fr",
voice="alloy",
on_audio=your_async_audio_callback, # async def cb(frame: rtc.AudioFrame): ...
on_error=your_async_error_callback, # optional
)
await translator.start()
await translator.push_frame(rtc_frame) # can call many times
await translator.aclose()
"""
def __init__(
self,
*,
target_language: str,
voice: str = "alloy",
on_audio: Callable[[rtc.AudioFrame], Awaitable[None]],
on_error: Optional[Callable[[Exception], Awaitable[None]]] = None,
modalities: list[str] | None = None,
temperature: float = 0.0,
):
self._target_language = target_language
self._voice = voice
self._on_audio = on_audio
self._on_error = on_error or (lambda e: asyncio.create_task(self._default_error(e)))
self._modalities = modalities or ["audio", "text"]
self._temperature = temperature
# RealtimeModel instance (OpenAI Realtime)
self._model = openai.realtime.RealtimeModel(
voice=self._voice,
modalities=self._modalities,
instructions=(
f"You are a real-time translator. The user speaks in any language. "
f"Translate what they say into {self._target_language} and respond by speaking the translation. "
"Do not add extra commentary."
),
temperature=self._temperature,
)
# Will be set in start()
self._session: Optional[openai.realtime.RealtimeSession] = None
self._input_audio_stream = None
self._running = False
self._recv_task: Optional[asyncio.Task] = None
async def _default_error(self, e: Exception) -> None:
logger.exception("Realtime translator error: %s", e)
async def start(self) -> None:
"""Create and connect a realtime session with the model."""
if self._running:
return
# create session object from model
# note: .create_session() or .session() is the pattern used in docs/examples
# adapt if your SDK exposes a different constructor (e.g., .session() vs .create_session()).
try:
# acquire a session object
# some SDK versions use `self._model.session()`; others use `await self._model.create_session()`.
# try both patterns for compatibility.
session = None
try:
session = await self._model.create_session()
except Exception:
session = self._model.session()
self._session = session
# get input audio stream handle to push frames
# the session exposes an input_audio_stream-like interface in examples
self._input_audio_stream = getattr(self._session, "input_audio_stream", None)
# hook events: when model publishes translated audio, call on_audio
def _on_track_published(ev):
try:
track = getattr(ev, "track", None)
if track is None:
return
# only handle audio tracks
if not isinstance(track, rtc.AudioTrack):
return
async def _consume_published_audio():
# read frames from the published track's stream
stream = rtc.AudioStream.from_track(track)
try:
async for frame_ev in stream:
# forward translated rtc.AudioFrame to callback
await self._on_audio(frame_ev.frame)
except Exception as ex:
await self._on_error(ex)
finally:
await stream.aclose()
asyncio.create_task(_consume_published_audio())
except Exception as ex:
asyncio.create_task(self._on_error(ex))
# common event names in examples: "track_published", "response_finished", "transcript", etc.
# attach handler(s)
self._session.on("track_published", _on_track_published)
# optional: handle text transcript events for subtitles/diagnostics
def _on_transcript(ev):
# ev may have .transcript / .text depending on SDK; keep non-blocking
text = getattr(ev, "transcript", None) or getattr(ev, "text", None)
if text:
logger.debug("translated text: %s", text)
self._session.on("input_audio_transcription_completed", _on_transcript)
self._session.on("response_text_delta", lambda ev: None)
# connect the session (some variants require await)
connect = getattr(self._session, "connect", None)
if asyncio.iscoroutinefunction(connect):
await connect()
elif callable(connect):
connect()
self._running = True
<http://logger.info|logger.info>("RealtimeLLMTranslator started")
except Exception as e:
await self._on_error(e)
raise
async def push_frame(self, frame: rtc.AudioFrame) -> None:
"""Push an rtc.AudioFrame into the model session for translation.
Call this repeatedly as you receive frames. This method is async to await
underlying stream backpressure.
"""
if not self._running or self._session is None:
raise RuntimeError("RealtimeLLMTranslator not started")
if self._input_audio_stream is None:
raise RuntimeError("Session does not expose input_audio_stream")
try:
# many examples show an input stream with .put_frame or .push_frame method
if hasattr(self._input_audio_stream, "put_frame"):
await self._input_audio_stream.put_frame(frame)
elif hasattr(self._input_audio_stream, "push_frame"):
await self._input_audio_stream.push_frame(frame)
else:
# fallback: try synchronous call
fn = getattr(self._input_audio_stream, "put_frame", None) or getattr(self._input_audio_stream, "push_frame", None)
if fn is None:
raise RuntimeError("input_audio_stream has no put_frame/push_frame")
res = fn(frame)
if asyncio.iscoroutine(res):
await res
except Exception as e:
await self._on_error(e)
raise
async def aclose(self) -> None:
"""Close session and stop background tasks."""
if not self._running:
return
self._running = False
try:
if self._session:
aclose = getattr(self._session, "aclose", None)
if asyncio.iscoroutinefunction(aclose):
await aclose()
elif callable(aclose):
aclose()
except Exception as e:
await self._on_error(e)
finally:
self._session = None
self._input_audio_stream = None
<http://logger.info|logger.info>("RealtimeLLMTranslator stopped")
Notes and integration tips
• SDK differences: LiveKit examples use RealtimeModel.session() or await RealtimeModel.create_session(). The snippet tries create_session() first and falls back to session() to maximize compatibility with different SDK releases. If your installed SDK has a single known API (.session() or .create_session()), you can simplify that part.
• Input audio stream method names vary: some examples show .put_frame(), others .push_frame(). The code attempts both.
• Event names in sessions: examples use "track_published", "input_audio_transcription_completed", and "response_text_delta". If your SDK uses different event names, replace them accordingly.
• The class assumes the model will publish an audio track (translated speech) which we consume via rtc.AudioStream.from_track(track) and forward frames to your on_audio callback as rtc.AudioFrame. This matches an audio-in → audio-out flow.
• If you prefer the model to output only text (so you handle TTS yourself), set modalities=["text"] on the RealtimeModel and then consume text events instead of published tracks.
If you want, I can:
• produce a tightened version targeted to your exact installed SDK method names (tell me whether your SDK exposes .create_session() or .session() and whether input audio stream uses put_frame or push_frame), or
• provide a text-only variant that returns translated text instead of translated audio.