little-kite-91953
02/16/2026, 12:16 PMtall-airline-16756
02/16/2026, 12:16 PMtall-airline-16756
02/16/2026, 12:16 PMsync_transcription is off.
Short answer: Yes — you can receive timing information even with sync_transcription=false, but only when you also enable aligned transcripts from TTS and your TTS provider supports it.
Details:
• Turning off sync_transcription makes LiveKit send the full transcription as soon as it’s available instead of streaming text in sync with audio.
• If you set use_tts_aligned_transcript = true in the AgentSession and your TTS provider supports aligned transcription forwarding (for example, Cartesia or ElevenLabs), the transcription payload includes TimedString items with start_time and end_time values (in seconds). You can use those timestamps to control when and how you reveal text in your UI.
• If use_tts_aligned_transcript is not enabled or the chosen TTS provider does not provide aligned timing, timestamps will not be available.
If you’d like, tell me:
• Which TTS provider you’re using (or planning to use), and
• Whether you already have use_tts_aligned_transcript enabled
I can then confirm what timing data to expect and show a sample of the transcription structure you’ll receive.
Sources: Text and transcriptions | LiveKit Documentation | ElevenLabs TTS plugin guide | LiveKit Documentation | Cartesia TTS plugin guide | LiveKit Documentationlittle-kite-91953
02/16/2026, 12:49 PMWe enabled use_tts_aligned_transcript=True and are using a TTS that supports aligned transcript (Cartesia with word_timestamps). The client still receives start_time=0, end_time=0 on every transcription segment from useTrackTranscription.
We traced this to livekit/agents/voice/room_io/_output.py: _ParticipantLegacyTranscriptionOutput._publish_transcription always passes start_time=0, end_time=0 into TranscriptionSegment and never uses the TimedString start/end times from the pipeline. So even when the pipeline produces timed text, the legacy transcription path doesn’t publish it.
Question: Is the intent that real timecodes be published when use_tts_aligned_transcript=True and the TTS provides aligned transcript? If yes, will the SDK be updated so that _publish_transcription (and/or the stream path) uses the pipeline’s TimedString times instead of 0,0? Or is there another supported way to get real segment timecodes to the client without patching the SDK?
tall-airline-16756
02/16/2026, 12:49 PMuseTrackTranscription even though you enabled use_tts_aligned_transcript=True and are using Cartesia with word_timestamps.
Short answers
• Yes — the intent is that real timestamps are published when use_tts_aligned_transcript=True and the TTS provider supplies aligned timings.
• Right now, the legacy transcription output (lk.transcription) does not preserve those timings: _ParticipantLegacyTranscriptionOutput._publish_transcription sets start_time=0 and end_time=0, so the client sees zeros. That behavior effectively discards the pipeline’s TimedString times.
• Workaround: you can surface the TimedString timings to the client without patching the core SDK by emitting them yourself from a transcription_node (or publishing them on a custom text/data stream). That bypasses the legacy path and gives you word/sentence timecodes the frontend can use.
Why this is happening
• The transcription pipeline does produce TimedString objects with start_time and end_time (relative to the agent response turn) when use_tts_aligned_transcript=True and the provider supports aligned transcripts.
• The legacy publishing function however constructs TranscriptionSegment with hardcoded zeros rather than using the TimedString fields, so timing metadata is not forwarded to lk.transcription.
Practical workaround (recommended)
• Add logic in your transcription_node to detect TimedString instances and publish them on a custom topic or data stream that your client listens to. Example approach:
1. In your agent pipeline transcription_node, forward TimedString objects by publishing a JSON payload (text, start_time, end_time) to a custom text/data topic (e.g., my_transcription_stream).
2. On the frontend, register a handler for that topic and use the included `start_time`/`end_time` to reveal text timed to your playback.
This avoids modifying LiveKit internals and gives you immediate access to accurate timings.
If you’d like, I can:
• Show a concise code snippet tailored to your runtime (Python or Node) that publishes TimedString events to a custom text stream.
• Describe the exact client-side read/handler code to consume and use those timestamps.
• Confirm any additional implementation details if you share which SDK and versions you’re using.
Would you like the code snippet for the transcription_node publishing approach in Python or in Node?
Sources: Text and transcriptions | LiveKit Documentation | Cartesia TTS plugin guide | LiveKit Documentation | Pipeline nodes and hooks | LiveKit Documentationlittle-kite-91953
02/16/2026, 12:50 PMThanks for confirming. We’d like the Python snippet for the transcription_node approach that publishes TimedString (text, start_time, end_time) to a custom topic, plus the client-side handler to consume it. We’re on the LiveKit Agents Python SDK (agents run in Python); frontend isReact/TypeScript with @alert-easter-19258/components-react and livekit-client. How this compares to the current patch Current monkey-patchLiveKit’s custom-stream approachWhere times are sentWe fix up the legacy path so publish_transcription() gets real start_time/end_time. Client keeps using useTrackTranscription(agentTrack) and existing segment handling.Agent sends TimedStrings to a custom topic (e.g. mytranscription_stream). Client subscribes to that topic instead of (or in addition to) track transcription.ProsNo frontend changes. Same API as today. One place to maintain (the patch).No SDK patching; supported pattern. You own the payload (could send word-level if you want).ConsDepends on SDK internals; could break on SDK upgrades.Requires agent-side “transcription_node” (or equivalent) to publish to the custom topic and frontend to listen and feed your caption/timecode logic from that stream.
tall-airline-16756
02/16/2026, 12:51 PMtranscription_node snippet that publishes TimedString payloads to a custom topic, plus a React/TypeScript client handler using livekit-client/@livekit/components-react to consume them.
Below are concise examples you can adapt. They show:
• Agent-side (Python) transcription_node that detects TimedString objects and publishes JSON messages with text, start_time, and end_time to a custom text stream topic (my_transcription_stream).
• Frontend (React/TypeScript) code that registers a text stream handler, parses the JSON, and shows how to incorporate timings into your captioning logic.
Agent-side (Python) — transcription_node
• This runs inside your Agents pipeline. It forwards TimedString items by publishing them as JSON on a custom topic. Adjust imports and session/participant access to match your codebase.
import json
from typing import AsyncIterable
from livekit import TimedString # adjust import if TimedString path differs
# Example transcription_node signature used by the Agents Python SDK
async def transcription_node(
self,
text: AsyncIterable[str | TimedString],
model_settings, # keep signature compatible with your pipeline
) -> AsyncIterable[str | TimedString]:
"""
For each TimedString from the pipeline, publish a JSON message to a custom
text stream topic 'my_transcription_stream'. Yield objects onward so the
rest of the pipeline still works.
"""
async for chunk in text:
# If the pipeline yields TimedString, it should have .text, .start_time, .end_time
if isinstance(chunk, TimedString):
payload = {
"type": "timed_transcription",
"text": chunk.text,
"start_time": chunk.start_time, # seconds (float)
"end_time": chunk.end_time, # seconds (float)
# optional: include sequence, confidence, word-level list, etc.
}
# publish as a text stream on a custom topic
try:
# session.room.local_participant is typically available in Agents code
await self.session.room.local_participant.publish_data(
json.dumps(payload),
topic="my_transcription_stream",
)
except Exception as e:
# Log and continue; do not block the pipeline on publish errors
self.logger.exception("failed to publish timed transcription: %s", e)
# also yield the TimedString onward (so other nodes still receive it)
yield chunk
else:
# plain string chunk
yield chunk
Notes:
• Replace self.session.room.local_participant.publish_data with the exact publish API available in your agent runtime if it differs.
• You can extend payload to include word_timestamps (list of words with start/end) if the pipeline provides them.
• Publishing every TimedString (word-level) vs. sentence-level: choose what best fits bandwidth and frontend rendering. If word-level, consider batching or coalescing small words into phrase segments to reduce message volume.
Frontend (React + TypeScript) — subscribe and consume
• Example uses Room from livekit-client. If you use @livekit/components-react hooks (e.g., useRoom), adapt to your setup.
• This registers a text stream handler for topic my_transcription_stream. It parses JSON payloads and emits events you can feed into your caption/timecode UI.
import { useEffect } from "react";
import { Room, DataPacket_Kind } from "livekit-client";
import { useRoom } from "@livekit/components-react";
type TimedTranscriptionPayload = {
type: "timed_transcription";
text: string;
start_time: number; // seconds
end_time: number; // seconds
// optional: word-level array, sequence id, etc.
};
export function useTimedTranscription(handler: (msg: TimedTranscriptionPayload) => void) {
const { room } = useRoom(); // or obtain Room instance another way
useEffect(() => {
if (!room) return;
// reader-based API: registerTextStreamHandler / registerDataReceiver
// Some SDK versions expose registerTextStreamHandler(topic, handler)
const topic = "my_transcription_stream";
const textHandler = async (reader: ReadableStreamDefaultReader<Uint8Array>) => {
// readAll convenience if available, else iterate
try {
// If the SDK provides `readAll()`:
// const raw = await reader.readAll();
// const ok = raw && raw.length > 0;
// For portability, we'll loop:
while (true) {
const { value, done } = await reader.read();
if (done) break;
const str = new TextDecoder().decode(value);
try {
const payload = JSON.parse(str) as TimedTranscriptionPayload;
handler(payload);
} catch (e) {
console.error("failed to parse timed transcription", e, str);
}
}
} catch (err) {
console.error("text stream handler error", err);
}
};
// Register the handler. API name may vary by client version:
// - room.registerTextStreamHandler(topic, textHandler)
// - room.on("dataReceived", ...) OR room.registerDataReceiver
// Try registerTextStreamHandler first:
let unregister: (() => void) | undefined;
if ((room as any).registerTextStreamHandler) {
(room as any).registerTextStreamHandler(topic, textHandler);
unregister = () => {
try {
(room as any).unregisterTextStreamHandler(topic);
} catch {}
};
} else if ((room as any).onDataReceived) {
// Fallback: listen for data messages and filter by topic
const onData = (payload: Uint8Array, kind: DataPacket_Kind, participant?: any, topics?: string[]) => {
// Only process if topic matches (topics param depends on SDK)
if (topics && topics.includes(topic)) {
const str = new TextDecoder().decode(payload);
try {
const parsed = JSON.parse(str) as TimedTranscriptionPayload;
handler(parsed);
} catch (e) {
console.error("failed to parse timed transcription", e, str);
}
}
};
room.on("dataReceived", onData);
unregister = () => room.off("dataReceived", onData);
} else {
console.warn("room.registerTextStreamHandler not available in this SDK version");
}
return () => {
if (unregister) unregister();
};
}, [room, handler]);
}
Using the payload in your caption UI
• The `start_time`/`end_time` in the payload are relative to the start of the agent’s response turn (seconds). Decide how you map those to your UI timeline:
◦ If you also play the agent audio via a track, align the agent audio playback start to the same reference (agent turn start) so start/end map directly.
◦ If you render text progressively, use start_time to schedule when each chunk appears and end_time for highlighting or fade-out.
Example simple usage in a component:
// pseudo-usage inside a React component
useTimedTranscription((payload) => {
// Store into local state or enqueue for timed reveal
enqueueCaption({
text: payload.text,
start: Date.now() + payload.start_time * 1000, // if mapping to absolute time
end: Date.now() + payload.end_time * 1000,
});
});
Other considerations and trade-offs
• This approach is supported and avoids patching the SDK internals. You own the payload format and can include word-level timing, sequences, confidence scores, etc.
• If you publish very frequent small messages (word-level), consider batching or coalescing to reduce data overhead.
• You’ll need both agent-side changes (the transcription_node) and frontend to subscribe to my_transcription_stream. This is a small, maintainable surface compared to monkey-patching the SDK.
• If you prefer zero frontend changes, the SDK legacy path should ideally be fixed so lk.transcription includes timing; you can continue using your existing client code once that is addressed. The workaround is immediate and stable.
If you want, I can:
• Provide a variant of the Python snippet that publishes batched phrase-level messages (coalescing words into a sentence chunk).
• Give a more exact frontend snippet tailored to the specific livekit-client version you’re using if you share your client SDK version.little-kite-91953
02/16/2026, 1:12 PMtall-airline-16756
02/16/2026, 1:12 PMlivekit/agents/voice/room_io/_output.py (the _ParticipantLegacyTranscriptionOutput._publish_transcription method) always passes start_time=0 and end_time=0 into TranscriptionSegment. That means timestamps produced by the pipeline’s TimedString objects are not forwarded to the lk.transcription client topic.
• The intent of use_tts_aligned_transcript=True is to enable aligned timing (word/sentence start/end times) when the TTS provider supplies them. Those TimedString times are produced by the pipeline but are not preserved by the legacy publishing path.
• Therefore a change is necessary in the SDK’s legacy output plumbing if you want lk.transcription (the existing client-side track/transcription API) to receive and expose real start/end times. Without that patch, the legacy topic will continue to show 0/0.
Options and recommendation
• Patch the legacy publish path so _publish_transcription uses the pipeline’s TimedString.start_time and TimedString.end_time when available. This preserves the existing client API (useTrackTranscription(agentTrack)) and requires no frontend changes.
• Or continue using the supported workaround I described earlier: publish timing-aware payloads from a transcription_node to a custom topic and have the frontend subscribe to that topic. This avoids changing the SDK but requires the client to consume the custom stream (or to keep your monkey-patch approach).
Which to choose
• If you want zero frontend change and a single, durable place to maintain, a patch to the legacy publishing path is the clean solution.
• If you need an immediate unpatched solution that is supported and under your control, use the transcription_node → custom topic approach.
If you want, I can:
• Draft the minimal SDK patch for _publish_transcription (illustrating exactly where to read TimedString and populate TranscriptionSegment) so your team can evaluate or submit it.
• Or provide the Python/TS snippets again tailored to your exact SDK versions for the workaround.
Which would you prefer me to prepare next: the minimal patch diff for the legacy path, or a refined agent/node + frontend example for your current setup?little-kite-91953
02/16/2026, 1:12 PMtall-airline-16756
02/16/2026, 1:13 PM