Skip to content

feat(elevenlabs): add applyLanguageTextNormalization TTS option#1427

Merged
toubatbrian merged 1 commit into
mainfrom
claude/quirky-galileo-Zejp5
May 8, 2026
Merged

feat(elevenlabs): add applyLanguageTextNormalization TTS option#1427
toubatbrian merged 1 commit into
mainfrom
claude/quirky-galileo-Zejp5

Conversation

@toubatbrian
Copy link
Copy Markdown
Contributor

Summary

Ports livekit/agents#5679 ((elevenlabs tts): add apply_language_text_normalization param) from the Python livekit-agents repo into agents-js.

This PR adds support for ElevenLabs' apply_language_text_normalization query parameter on the multi-stream WebSocket URL. When set to true, ElevenLabs applies language-aware text normalization, which helps with proper pronunciation of text in some supported languages.

cc @toubatbrian @livekit/agent-devs for review.

Ported features

applyLanguageTextNormalization option on TTS (plugins/elevenlabs/src/tts.ts)

New optional applyLanguageTextNormalization?: boolean field on TTSOptions. When provided, it is appended to the multi-stream-input WebSocket URL as apply_language_text_normalization=<true|false>. When omitted, the parameter is not sent at all (preserving ElevenLabs' server-side default).

// Ref: python livekit-plugins/livekit-plugins-elevenlabs/livekit/plugins/elevenlabs/tts.py - 112 line
applyLanguageTextNormalization?: boolean;

The option flows through the same path as the existing applyTextNormalization option:

  1. Surfaced on the public TTSOptions interface.
  2. Stored on the internal ResolvedTTSOptions (kept optional so the URL builder can detect "not given").
  3. Conditionally appended in multiStreamUrl(...) only when defined, matching Python's is_given(...) guard.

Implementation nuances vs Python

  • NotGivenOr[bool] mapping: Python represents "not given" with the sentinel NOT_GIVEN and gates URL inclusion on is_given(...). JS uses boolean | undefined and gates on !== undefined. Behavior is equivalent: when the user does not pass the option, the query parameter is omitted from the WebSocket URL and ElevenLabs falls back to its server-side default.
  • Boolean serialization: Python uses str(value).lower() to produce "true" / "false". JS template-literal interpolation of a boolean already yields "true" / "false" lowercase, so no explicit conversion is needed.
  • Option naming: snake_case apply_language_text_normalization (Python field & wire param) maps to camelCase applyLanguageTextNormalization in JS. The wire query parameter remains apply_language_text_normalization.
  • ChunkedStream (REST /text-to-speech/.../stream) not modified: The Python diff only touches the multi-stream WebSocket URL builder (_multi_stream_url); the REST synthesize_url is unchanged. Mirroring that exactly here — only multiStreamUrl learns the new param.
  • No updateOptions change: The Python PR does not add this field to runtime update_options either, so it is a constructor-only knob in JS for parity.

Files changed

File Change
plugins/elevenlabs/src/tts.ts Add applyLanguageTextNormalization?: boolean to TTSOptions and ResolvedTTSOptions, propagate from constructor, conditionally append apply_language_text_normalization=... to the multi-stream URL.
.changeset/elevenlabs-language-text-normalization.md patch changeset for @livekit/agents-plugin-elevenlabs.

Test plan

  • pnpm --filter '@livekit/agents-plugin-elevenlabs...' build succeeds
  • pnpm format:check passes
  • pnpm --filter '@livekit/agents-plugin-elevenlabs' lint passes
  • Manual: instantiate new TTS({ applyLanguageTextNormalization: true }), run a streaming synthesis on a non-English voice, and confirm the WebSocket URL includes apply_language_text_normalization=true and pronunciation is improved
  • Manual: instantiate new TTS() without the option and confirm the URL omits the parameter (current behavior unchanged)

This PR was created by an automated Claude Code Routine maintained by @toubatbrian. The routine is currently in experimentation stage.


Generated by Claude Code

Ports livekit/agents#5679 to expose the ElevenLabs
`apply_language_text_normalization` query parameter on the multi-stream
WebSocket URL. The new option is optional and only sent when explicitly
provided, matching Python's NotGivenOr semantics.
@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fd9c37b582

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

params.push(`inactivity_timeout=${opts.inactivityTimeout}`);
params.push(`apply_text_normalization=${opts.applyTextNormalization}`);
if (opts.applyLanguageTextNormalization !== undefined) {
params.push(`apply_language_text_normalization=${opts.applyLanguageTextNormalization}`);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Send language normalization on the HTTP TTS path

When callers use new TTS({ applyLanguageTextNormalization: true }).synthesize(...), the option is never sent: ChunkedStream posts to synthesizeUrl() and the JSON body at run() only includes text, model_id, and voice_settings. The ElevenLabs Stream speech endpoint is the documented path that accepts apply_language_text_normalization, while this change only appends it to the multi-context WebSocket URL, so one-shot/chunked synthesis still runs with the API default (false) for the Japanese normalization case this option is meant to enable.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 1 additional finding.

Open in Devin Review

@toubatbrian toubatbrian merged commit 340d18c into main May 8, 2026
8 of 9 checks passed
@toubatbrian toubatbrian deleted the claude/quirky-galileo-Zejp5 branch May 8, 2026 07:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants