Improving docstring based on the recent changes.

This commit is contained in:
filipi87
2026-03-25 17:52:05 -03:00
parent b4096f9a11
commit 2ff4b3f4a3

View File

@@ -1151,19 +1151,15 @@ class TTSService(AIService):
async def _add_word_timestamps( async def _add_word_timestamps(
self, word_times: List[Tuple[str, float]], context_id: Optional[str] = None self, word_times: List[Tuple[str, float]], context_id: Optional[str] = None
): ):
"""Process word timestamps directly, building and pushing frames inline. """Process word timestamps directly, building and pushing TTSTextFrames inline.
This is the single processing path for all word timestamp events, used both Used both from _handle_audio_context (via _WordTimestampEntry) and from services
from _handle_audio_context (via _WordTimestampEntry) and from services that that do not use audio contexts. Each entry emits a TTSTextFrame with a PTS
do not use audio contexts. Sentinel entries drive control-frame emission: relative to the baseline established by start_word_timestamps().
- ("Reset", 0): reset timestamp baseline; emit LLMFullResponseEndFrame if needed. When the baseline (_initial_word_timestamp) is not yet set, entries are cached
- ("TTSStoppedFrame", 0): emit TTSStoppedFrame. in _initial_word_times and flushed once start_word_timestamps() is called
- Any other entry: emit TTSTextFrame with a PTS relative to the baseline. (i.e. when the first audio chunk is received).
When the baseline (_initial_word_timestamp) is not yet set, regular word entries
are cached in _initial_word_times and flushed once start_word_timestamps() is
called (i.e. when the first audio chunk is received).
""" """
for word, timestamp in word_times: for word, timestamp in word_times:
ts_ns = seconds_to_nanoseconds(timestamp) ts_ns = seconds_to_nanoseconds(timestamp)
@@ -1198,11 +1194,16 @@ class TTSService(AIService):
async def append_to_audio_context( async def append_to_audio_context(
self, context_id: str, frame: Frame | _WordTimestampEntry | None self, context_id: str, frame: Frame | _WordTimestampEntry | None
): ):
"""Append audio or control frame to an existing context. """Append a frame or word-timestamp entry to an existing audio context queue.
Passing ``None`` signals end-of-context (used by remove_audio_context to mark
the queue for deletion). If the context no longer exists but the context_id
matches the active turn, the context is transparently recreated before appending.
Args: Args:
context_id: The context to append audio to. context_id: The context to append to.
frame: The audio or control frame to append. frame: The frame, word-timestamp entry, or ``None`` (end-of-context sentinel)
to append.
""" """
if not context_id: if not context_id:
logger.debug(f"{self} unable to append audio to context: no context ID provided") logger.debug(f"{self} unable to append audio to context: no context ID provided")
@@ -1337,6 +1338,14 @@ class TTSService(AIService):
self._serialization_queue.task_done() self._serialization_queue.task_done()
async def _maybe_reset_word_timestamps(self): async def _maybe_reset_word_timestamps(self):
"""Reset word-timestamp state and emit LLMFullResponseEndFrame if needed.
Called at the end of an audio context (either on clean completion timeout or
when the context queue is drained). Resets the PTS baseline so the next turn
starts fresh. If an LLM response is still marked as in-progress and text frames
are not being pushed (which would have already emitted the frame), an
LLMFullResponseEndFrame is pushed with the PTS of the last word frame.
"""
await self.reset_word_timestamps() await self.reset_word_timestamps()
# If self._push_text_frames is True, we have already pushed the original LLMFullResponseEndFrame # If self._push_text_frames is True, we have already pushed the original LLMFullResponseEndFrame
if self._llm_response_started and not self._push_text_frames: if self._llm_response_started and not self._push_text_frames:
@@ -1360,9 +1369,8 @@ class TTSService(AIService):
elif frame is None: elif frame is None:
running = False running = False
elif isinstance(frame, _WordTimestampEntry): elif isinstance(frame, _WordTimestampEntry):
# _add_word_timestamps is the single processing path: it handles # Route word timestamps through _add_word_timestamps so they are
# sentinel entries ("Reset", "TTSStoppedFrame") and regular words # processed in playback order alongside audio frames.
# inline, keeping all word-frame logic in one place.
await self._add_word_timestamps( await self._add_word_timestamps(
[(frame.word, frame.timestamp)], frame.context_id [(frame.word, frame.timestamp)], frame.context_id
) )
@@ -1392,11 +1400,11 @@ class TTSService(AIService):
logger.trace(f"{self} time out on audio context {context_id}") logger.trace(f"{self} time out on audio context {context_id}")
if should_push_stop_frame and self._push_stop_frames: if should_push_stop_frame and self._push_stop_frames:
await self.push_frame(TTSStoppedFrame(context_id=context_id)) await self.push_frame(TTSStoppedFrame(context_id=context_id))
await self._maybe_reset_word_timestamps()
break break
if should_push_stop_frame and self._push_stop_frames: if should_push_stop_frame and self._push_stop_frames:
await self.push_frame(TTSStoppedFrame(context_id=context_id)) await self.push_frame(TTSStoppedFrame(context_id=context_id))
await self._maybe_reset_word_timestamps()
async def on_audio_context_interrupted(self, context_id: str): async def on_audio_context_interrupted(self, context_id: str):
"""Called when an audio context is cancelled due to an interruption. """Called when an audio context is cancelled due to an interruption.