Skip to content

examples: stabilize LiveKit pronunciation with Fish Audio - #2279

Open
rosetta-livekit-bot[bot] wants to merge 1 commit into
slogs-niceties-oxymoronfrom
trimmest-pampers-aspirate
Open

examples: stabilize LiveKit pronunciation with Fish Audio#2279
rosetta-livekit-bot[bot] wants to merge 1 commit into
slogs-niceties-oxymoronfrom
trimmest-pampers-aspirate

Conversation

@rosetta-livekit-bot

@rosetta-livekit-bot rosetta-livekit-bot Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Summary

  • rewrite LiveKit as Lyve Kit only in the Fish Audio TTS stream
  • support straight and curly possessives without changing the LLM response or transcript
  • wire the pronunciation transform after markdown and emoji filtering

Ports livekit/agents#6825.

Source diff coverage
  • Adapted: examples/homepage/agent.py -> examples/src/homepage/agent.ts. Imported the TypeScript transform and added it to the existing JS ttsTextTransforms option with the equivalent built-in markdown and emoji filters.
  • Adapted: examples/homepage/filters/pronunciation.py -> examples/src/homepage/filters/pronunciation.ts. Ported the Fish Audio-friendly Lyve Kit spellings and possessive matching while using the target framework's required ReadableStream transform signature and retaining chunk-boundary buffering.
  • Adapted: examples/homepage/tests/unit/test_pronunciation.py -> examples/src/homepage/tests/unit/pronunciation.test.ts. Ported the updated expected values and both new possessive cases to Vitest.
  • Not applicable: none. Every source file has a target counterpart and was ported.

Validation

  • pnpm build passes (40/40 workspace tasks).
  • pnpm exec vitest run --root . examples/src/homepage/tests/unit passes (9/9 tests).
  • Touched-file ESLint and Prettier checks pass.
  • cue-cli voice smoke test passes: the agent reached speaking, LiveKit Inference Fish Audio synthesized 10.655s of audio, the transcript retained LiveKit, and no framework error was emitted.
  • Full examples suite was run twice. Latest result: 105 passed, 2 skipped, 1 unrelated existing failure in examples/src/testing/agent_task.test.ts (missing FunctionCallOutputEvent after a task handoff), plus pre-existing async fake-LLM errors.
  • Full lint was run. It is blocked by pre-existing formatting failures in plugins/xai/src/stt.ts and examples/src/hotel-receptionist/hotel_receptionist.ts; the files in this PR pass ESLint.

No changeset is included because only the private, changeset-ignored examples package changed.


Ported from livekit/agents#6825

Original PR description

Fish Audio intermittently ignores phoneme controls and pronounces LiveKit like “live in California.” Use the TTS-only spelling “Lyve Kit” to preserve the intended “live music” sound without changing the LLM response or user-facing transcript.

Addresses MARKT-1004

Initial prompt and agent context

Model: GPT-5.6

homepage agent is using fish audio, and we should replace LiveKit or LiveKit's with https://docs.fish.audio/developer-guide/core-features/fine-grained-control#phoneme-control

it is live as in live music

I tried it on their website. it is a coin flip with <|phoneme_start|>L AY1 V<|phoneme_end|>

Lyve Kit feels close enough

@rosetta-livekit-bot
rosetta-livekit-bot Bot requested a review from a team as a code owner August 12, 2026 17:13
@changeset-bot

changeset-bot Bot commented Aug 12, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 97652d3

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

Open in Devin Review

Comment on lines +6 to +12
export const LIVEKIT_PRONUNCIATION = 'Lyve Kit';
export const LIVEKITS_PRONUNCIATION = "Lyve Kit's";
const LIVEKIT_RE = /\blivekit(?<possessive>['’]s)?\b/gi;

function replaceLiveKit(match: string, possessive: string | undefined): string {
return possessive ? LIVEKITS_PRONUNCIATION : LIVEKIT_PRONUNCIATION;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 New helper function and exported pronunciation values ship without documentation comments

The newly added replacement helper (replaceLiveKit at examples/src/homepage/filters/pronunciation.ts:10) and the two new exported pronunciation values have no documentation comment, while the value they replaced previously carried one, so the repository's requirement that every new addition be documented is not met.
Impact: Generated API docs lose the explanation of why these spellings exist, making the pronunciation workaround harder to understand later.

Documentation convention from CONTRIBUTING.md

CONTRIBUTING.md states: "If writing new methods/interfaces/enums/classes, document them. This project uses TypeDoc ... and every new addition has to be properly documented." The removed LIVEKIT_IPA had /** Inworld custom pronunciation for the word "LiveKit". */; the new LIVEKIT_PRONUNCIATION, LIVEKITS_PRONUNCIATION, and replaceLiveKit have none.

Suggested change
export const LIVEKIT_PRONUNCIATION = 'Lyve Kit';
export const LIVEKITS_PRONUNCIATION = "Lyve Kit's";
const LIVEKIT_RE = /\blivekit(?<possessive>[']s)?\b/gi;
function replaceLiveKit(match: string, possessive: string | undefined): string {
return possessive ? LIVEKITS_PRONUNCIATION : LIVEKIT_PRONUNCIATION;
}
/** Fish Audio-friendly spelling for the word "LiveKit". */
export const LIVEKIT_PRONUNCIATION = 'Lyve Kit';
/** Fish Audio-friendly spelling for the possessive form of "LiveKit". */
export const LIVEKITS_PRONUNCIATION = "Lyve Kit's";
const LIVEKIT_RE = /\blivekit(?<possessive>[']s)?\b/gi;
/** Pick the plain or possessive pronunciation for a matched "LiveKit" occurrence. */
function replaceLiveKit(_match: string, possessive: string | undefined): string {
return possessive ? LIVEKITS_PRONUNCIATION : LIVEKIT_PRONUNCIATION;
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Member

Rosetta port review: livekit/agents#6825 → this PR

Compared against the original Python PR (examples/homepage/{agent.py,filters/pronunciation.py,tests/unit/test_pronunciation.py}) for logic equivalence, error handling, and API compatibility.

Verification details (click to expand)

Logic equivalence

  • LIVEKIT_PRONUNCIATION / LIVEKITS_PRONUNCIATION constants match ("Lyve Kit" / "Lyve Kit's").
  • Regex \blivekit(['’]s)?\b ported with equivalent case-insensitivity — Python's re.IGNORECASE becomes JS's i flag, and the required g flag (JS String#replace isn't global-by-default the way re.sub is) is present.
  • Possessive branch (replaceLiveKit / _replace_livekit) is logically identical.
  • wholeWords/_whole_words word-boundary buffering is untouched by this PR (already ported in an earlier PR).
  • agent.ts/agent.py wiring: same transform order — filter_markdown, filter_emoji, then the pronunciation fn.
  • Test parity: both new possessive cases (straight ' and curly apostrophe) are present in both suites, plus the pre-existing negative case (LiveKitten).

API compatibility

  • pronounceLiveKit changed from AsyncIterable<string> → AsyncIterable<string> to ReadableStream<string> → ReadableStream<string>. This is correct and required: TextTransform in agents/src/voice/transcription/text_transforms.ts requires exactly (text: ReadableStream<string>) => ReadableStream<string>, matching filterMarkdown/filterEmoji.

Worth a look (non-blocking)

The sibling transforms in text_transforms.ts (filterMarkdown, filterEmoji, replace) all consume their input via the readStream(text) helper, which explicitly handles reader cleanup/stream-release and accepts an AbortSignal. pronounceLiveKit instead feeds the raw ReadableStream<string> straight into wholeWords(chunks: AsyncIterable<string>), relying on Node's ReadableStream supporting Symbol.asyncIterator directly. It builds and the tests pass, so it's functionally fine, but it bypasses the framework's established cleanup/cancellation path — might be worth switching to readStream(text) for consistency and to avoid a locked reader if the utterance is aborted mid-stream.

No missing features or behavioral bugs found. Output-stream error handling (controller.error) is present and appropriate.

Review effort: low — 3 files, ~40 lines, a direct line-for-line port with one non-blocking stream-handling nit.


Generated by Claude Code

@chenghao-mou chenghao-mou added the effort:low label Aug 12, 2026 — with Claude
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant