Merge pull request #4475 from pipecat-ai/mb/cartesia-korean-fix

Fix Cartesia CJK timestamp spacing
This commit is contained in:
Mark Backman
2026-05-13 13:20:42 -04:00
committed by GitHub
4 changed files with 136 additions and 12 deletions

View File

@@ -419,30 +419,30 @@ class CartesiaTTSService(WebsocketTTSService):
"""Convenience method to create a speed tag."""
return f'<speed ratio="{speed}" />'
def _is_cjk_language(self, language: str) -> bool:
"""Check if the given language is CJK (Chinese, Japanese, Korean).
def _is_chinese_or_japanese_language(self, language: str) -> bool:
"""Check if the given language is Chinese or Japanese.
Args:
language: The language code to check.
Returns:
True if the language is Chinese, Japanese, or Korean.
True if the language is Chinese or Japanese.
"""
cjk_languages = {"zh", "ja", "ko"}
base_lang = language.split("-")[0].lower()
return base_lang in cjk_languages
return base_lang in {"zh", "ja"}
def _process_word_timestamps_for_language(
self, words: list[str], starts: list[float]
) -> list[tuple[str, float]]:
"""Process word timestamps based on the current language.
For CJK languages, Cartesia groups related characters in the same timestamp message.
For Chinese and Japanese, Cartesia groups related characters in the same timestamp
message.
For example, in Japanese a single message might be `['', '', '', '', '', '']`.
We combine these into single words so the downstream aggregator can add natural
spacing between meaningful units rather than individual characters.
For non-CJK languages, words are already properly separated and are used as-is.
For other languages, words are already properly separated and are used as-is.
Args:
words: List of words/characters from Cartesia.
@@ -453,10 +453,10 @@ class CartesiaTTSService(WebsocketTTSService):
"""
current_language = assert_given(self._settings.language)
# Check if this is a CJK language (if language is None, treat as non-CJK)
if current_language and self._is_cjk_language(current_language):
# For CJK languages, combine all characters in this message into one word
# using the first character's start time
# Check if this is a Chinese/Japanese language (if language is None, treat as other)
if current_language and self._is_chinese_or_japanese_language(current_language):
# For Chinese/Japanese, combine all characters in this message into one word
# using the first character's start time.
if words and starts:
combined_word = "".join(words)
first_start = starts[0]
@@ -467,6 +467,11 @@ class CartesiaTTSService(WebsocketTTSService):
# For non-CJK languages, use as-is
return list(zip(words, starts))
def _word_timestamps_include_inter_frame_spaces(self) -> bool:
"""Whether timestamp text should be treated as carrying its own spacing."""
current_language = assert_given(self._settings.language)
return bool(current_language and self._is_chinese_or_japanese_language(current_language))
def _build_msg(
self,
text: str = "",
@@ -660,7 +665,13 @@ class CartesiaTTSService(WebsocketTTSService):
processed_timestamps = self._process_word_timestamps_for_language(
msg["word_timestamps"]["words"], msg["word_timestamps"]["start"]
)
await self.add_word_timestamps(processed_timestamps, ctx_id)
await self.add_word_timestamps(
processed_timestamps,
ctx_id,
includes_inter_frame_spaces=(
True if self._word_timestamps_include_inter_frame_spaces() else None
),
)
elif msg["type"] == "chunk":
frame = TTSAudioRawFrame(
audio=base64.b64decode(msg["data"]),