fix: AzureTTSService punctuation spacing

This commit is contained in:
Mark Backman
2026-01-17 08:17:06 -05:00
parent f7d3e63063
commit 043403fe23
2 changed files with 43 additions and 7 deletions

1
changelog/3489.fixed.md Normal file
View File

@@ -0,0 +1 @@
- Fixed `AzureTTSService` transcript formatting where punctuation appeared with extra spaces (e.g., "Hello !" instead of "Hello!"). Azure sends punctuation as separate word boundaries, which are now merged with preceding words to produce properly formatted transcripts across all languages.

View File

@@ -277,6 +277,8 @@ class AzureTTSService(WordTTSService, AzureBaseTTSService):
self._started = False
self._first_chunk = True
self._cumulative_audio_offset: float = 0.0 # Cumulative audio duration in seconds
self._last_word: Optional[str] = None # Track last word for punctuation merging
self._last_timestamp: Optional[float] = None # Track last timestamp
def can_generate_metrics(self) -> bool:
"""Check if this service can generate processing metrics.
@@ -346,9 +348,24 @@ class AzureTTSService(WordTTSService, AzureBaseTTSService):
await self.cancel_task(self._word_processor_task)
self._word_processor_task = None
def _is_punctuation_only(self, text: str) -> bool:
"""Check if text consists only of punctuation and whitespace.
Args:
text: Text to check.
Returns:
True if text is only punctuation/whitespace, False otherwise.
"""
return text and all(not c.isalnum() for c in text)
def _handle_word_boundary(self, evt):
"""Handle word boundary events from Azure SDK.
Azure sends punctuation as separate word boundaries, which causes
spacing issues in the final transcript. This method merges punctuation
with the previous word to maintain proper formatting.
Args:
evt: SpeechSynthesisWordBoundaryEventArgs from Azure Speech SDK
containing word text and audio offset timing.
@@ -362,13 +379,23 @@ class AzureTTSService(WordTTSService, AzureBaseTTSService):
# Add cumulative offset to get absolute timestamp across sentences
absolute_seconds = self._cumulative_audio_offset + sentence_relative_seconds
# Queue word timestamp for async processing
# Use thread-safe queue since this is called from Azure SDK thread
if word:
logger.trace(f"{self}: Word boundary - '{word}' at {absolute_seconds:.2f}s")
# Put in temporary queue - will be processed by async task
# Store as (word, timestamp_in_seconds) tuple
self._word_boundary_queue.put_nowait((word, absolute_seconds))
if not word:
return
# Check if this is punctuation-only
is_punctuation = self._is_punctuation_only(word)
if is_punctuation and self._last_word is not None:
# Merge punctuation with the previous word (don't queue yet, more punctuation might follow)
self._last_word += word
else:
# This is a real word. First, queue any pending word from before.
if self._last_word is not None:
self._word_boundary_queue.put_nowait((self._last_word, self._last_timestamp))
# Now store this new word for next time
self._last_word = word
self._last_timestamp = absolute_seconds
async def _word_processor_task_handler(self):
"""Process word timestamps from the queue and call add_word_timestamps."""
@@ -397,6 +424,12 @@ class AzureTTSService(WordTTSService, AzureBaseTTSService):
Args:
evt: Completion event from Azure Speech SDK.
"""
# Flush any pending word before completing
if self._last_word is not None:
self._word_boundary_queue.put_nowait((self._last_word, self._last_timestamp))
self._last_word = None
self._last_timestamp = None
# Update cumulative audio offset for next sentence
if evt.result and evt.result.audio_duration:
self._cumulative_audio_offset += evt.result.audio_duration.total_seconds()
@@ -435,6 +468,8 @@ class AzureTTSService(WordTTSService, AzureBaseTTSService):
self._started = False
self._first_chunk = True
self._cumulative_audio_offset = 0.0
self._last_word = None
self._last_timestamp = None
async def flush_audio(self):
"""Flush any pending audio data."""