Merge pull request #2304 from pipecat-ai/mb/cartesia-cjk-lang-support

CartesiaTTSService: Add CJK lang support for word timestamps
This commit is contained in:
Mark Backman
2025-08-05 14:08:53 -07:00
committed by GitHub
2 changed files with 55 additions and 2 deletions

View File

@@ -32,6 +32,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
average response time between user stop and bot start, as well as minimum and average response time between user stop and bot start, as well as minimum and
maximum response latency. maximum response latency.
- Added Chinese, Japanese, Korean word timestamp support to
`CartesiaTTSService`.
### Changed ### Changed
- Changed the default model for both `ElevenLabsTTSService` and - Changed the default model for both `ElevenLabsTTSService` and

View File

@@ -216,6 +216,54 @@ class CartesiaTTSService(AudioContextWordTTSService):
""" """
return language_to_cartesia_language(language) return language_to_cartesia_language(language)
def _is_cjk_language(self, language: str) -> bool:
"""Check if the given language is CJK (Chinese, Japanese, Korean).
Args:
language: The language code to check.
Returns:
True if the language is Chinese, Japanese, or Korean.
"""
cjk_languages = {"zh", "ja", "ko"}
base_lang = language.split("-")[0].lower()
return base_lang in cjk_languages
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 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.
Args:
words: List of words/characters from Cartesia.
starts: List of start timestamps for each word/character.
Returns:
List of (word, start_time) tuples processed for the language.
"""
current_language = self._settings.get("language", "en")
# Check if this is a CJK language
if 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
if words and starts:
combined_word = "".join(words)
first_start = starts[0]
return [(combined_word, first_start)]
else:
return []
else:
# For non-CJK languages, use as-is
return list(zip(words, starts))
def _build_msg( def _build_msg(
self, text: str = "", continue_transcript: bool = True, add_timestamps: bool = True self, text: str = "", continue_transcript: bool = True, add_timestamps: bool = True
): ):
@@ -351,9 +399,11 @@ class CartesiaTTSService(AudioContextWordTTSService):
await self.add_word_timestamps([("TTSStoppedFrame", 0), ("Reset", 0)]) await self.add_word_timestamps([("TTSStoppedFrame", 0), ("Reset", 0)])
await self.remove_audio_context(msg["context_id"]) await self.remove_audio_context(msg["context_id"])
elif msg["type"] == "timestamps": elif msg["type"] == "timestamps":
await self.add_word_timestamps( # Process the timestamps based on language before adding them
list(zip(msg["word_timestamps"]["words"], msg["word_timestamps"]["start"])) processed_timestamps = self._process_word_timestamps_for_language(
msg["word_timestamps"]["words"], msg["word_timestamps"]["start"]
) )
await self.add_word_timestamps(processed_timestamps)
elif msg["type"] == "chunk": elif msg["type"] == "chunk":
await self.stop_ttfb_metrics() await self.stop_ttfb_metrics()
self.start_word_timestamps() self.start_word_timestamps()