Skip to content

examples: switch homepage agent to Fish Audio with expressive mode - #2278

Closed
rosetta-livekit-bot[bot] wants to merge 2 commits into
moire-apply-yearliesfrom
slogs-niceties-oxymoron
Closed

examples: switch homepage agent to Fish Audio with expressive mode#2278
rosetta-livekit-bot[bot] wants to merge 2 commits into
moire-apply-yearliesfrom
slogs-niceties-oxymoron

Conversation

@rosetta-livekit-bot

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

Copy link
Copy Markdown
Contributor

Ports livekit/agents#6822 to the TypeScript homepage example.

Summary

  • switch the homepage TTS to Fish Audio S2.1 Pro with the Marley voice
  • enable expressive mode and publish the human-facing voice label
  • remove the Inworld-specific pronunciation transform
  • include the target-native expressive-mode infrastructure from agents-js#2267 because the stacked base predates it
Source diff coverage
  • Adapted: examples/homepage/README.md -> examples/src/homepage/README.md. Updated the provider and expressive-mode description; retained Krisp as the target counterpart to Python's ai-coustics isolation.
  • Adapted: examples/homepage/agent.py -> examples/src/homepage/agent.ts. Ported the Fish Audio model, Marley voice ID and label, expressive: true, frontend label publication, and removal of the Inworld pronunciation hook into the target's TypeScript APIs.
  • Adapted: examples/homepage/tests/unit/test_agent_config.py -> examples/src/homepage/tests/unit/agent_config.test.ts. Ported the source assertions to the existing Vitest config test.
  • Not applicable: none.

Validation

  • pnpm build passed (all 40 workspace packages)
  • pnpm lint passed
  • pnpm format:check passed
  • pnpm test agents passed: 114 files, 1640 tests, 5 skipped
  • pnpm exec vitest run examples/src/homepage/tests/unit passed: 5 files, 9 tests
  • cue-cli voice-mode validation passed, asserting Fish Audio S2.1 Pro usage and expressive <expr> markers in framework events
  • pnpm --filter livekit-agents-examples test was run twice. The isolated rerun had 105 passing tests, 2 skipped, and one failure in the pre-existing examples/src/testing/agent_task.test.ts assertion that expects a function-call output after an agent handoff; unrelated asynchronous errors were also reported by survey_agent.test.ts and run_result.test.ts.

Ported from livekit/agents#6822

Original PR description

No description.

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

changeset-bot Bot commented Aug 12, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 210acaa

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 39 packages
Name Type
@livekit/agents Patch
@livekit/agents-plugin-anam Patch
@livekit/agents-plugin-anthropic Patch
@livekit/agents-plugin-assemblyai Patch
@livekit/agents-plugin-azure Patch
@livekit/agents-plugin-baseten Patch
@livekit/agents-plugin-bey Patch
@livekit/agents-plugin-cartesia Patch
@livekit/agents-plugin-cerebras Patch
@livekit/agents-plugin-deepgram Patch
@livekit/agents-plugin-did Patch
@livekit/agents-plugin-elevenlabs Patch
@livekit/agents-plugin-fishaudio Patch
@livekit/agents-plugin-google Patch
@livekit/agents-plugin-hedra Patch
@livekit/agents-plugin-hume Patch
@livekit/agents-plugin-inworld Patch
@livekit/agents-plugin-krisp Patch
@livekit/agents-plugin-lemonslice Patch
@livekit/agents-plugin-liveavatar Patch
@livekit/agents-plugin-livekit Patch
@livekit/agents-plugin-minimax Patch
@livekit/agents-plugin-mistral Patch
@livekit/agents-plugin-mistralai Patch
@livekit/agents-plugin-neuphonic Patch
@livekit/agents-plugin-openai Patch
@livekit/agents-plugin-perplexity Patch
@livekit/agents-plugin-phonic Patch
@livekit/agents-plugin-protoface Patch
@livekit/agents-plugin-resemble Patch
@livekit/agents-plugin-rime Patch
@livekit/agents-plugin-runway Patch
@livekit/agents-plugin-sarvam Patch
@livekit/agents-plugin-silero Patch
@livekit/agents-plugin-soniox Patch
@livekit/agents-plugin-tavus Patch
@livekit/agents-plugins-test Patch
@livekit/agents-plugin-trugen Patch
@livekit/agents-plugin-xai Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another 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.

View 2 additional findings in Devin Review.

Open in Devin Review

Comment on lines +1449 to +1460
export function normalizeMarkup(provider: string, text: string): string {
let out = text;
if (provider in PROVIDER_MARKUP) {
out = out.replace(EXPR_UNCLOSED_RE, '$1/>');
}
const tags = SELF_CLOSING_TAGS[provider];
if (!tags) {
return out;
}
const pattern = new RegExp(`<(${tags.map(escapeRegExp).join('|')})\\b([^>]*[^/])\\s*>`, 'g');
return out.replace(pattern, '<$1$2/>');
}

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.

🟡 A leftover closing tag can be spoken aloud when the model writes a wrapping delivery tag

When the model writes a delivery tag that wraps words instead of standing alone, only its opening half is repaired (normalizeMarkup at agents/src/tts/provider_format.ts:1449-1460) and the unmatched closing half is never removed before the sentence is sent to the voice, so listeners can hear it read out.
Impact: The agent may audibly say something like "slash expression" in the middle of a sentence.

How the repair leaves an orphan closing tag on the audio path

The code explicitly anticipates models writing the wrapping form (see the comment on ATTRIBUTE_MARKUP_TAGS, agents/src/tts/provider_format.ts:1180-1192: "models do write <expression value="warm">words</expression> ... which is exactly why normalizeMarkup repairs that shape").

For input <expression value="warm">Hello there</expression> with provider fishaudio/inworld/cartesia:

  1. normalizeMarkup matches <(expression)\b([^>]*[^/])\s*> and rewrites the opening tag to <expression value="warm"/>. The trailing </expression> is not matched by any pattern here.
  2. convertMarkup (agents/src/tts/provider_format.ts:1463-1489) then lowers the now self-closing tag (FISHAUDIO_EXPRESSION_RE / convertExpressionTags), but neither of those patterns — nor convertExpr, which only drops stray </expr> — removes a stray native </expression>.
  3. The result [very warm]Hello there</expression> is what inference/tts.ts sends as input_transcript (agents/src/inference/tts.ts:697-705, which calls convert(normalize(...))).

Note the un-normalized path is also lossy: without step 1, FISHAUDIO_EXPRESSION_RE's >(?:.*?)</expression> alternative would swallow the wrapped words entirely. A final sweep that drops any remaining unmatched native closing tag (mirroring what convertExpr already does for </expr>) would fix both.

Prompt for agents
In agents/src/tts/provider_format.ts, normalizeMarkup() repairs a model-written wrapping tag such as `<expression value="warm">Hello there</expression>` by rewriting only the opening tag to self-closing form. The matching `</expression>` is left in the text, and nothing downstream removes it: convertExpr only drops stray `</expr>` delimiters, and the per-provider conversions (FISHAUDIO_EXPRESSION_RE, convertExpressionTags, XAI/FISHAUDIO break and emphasis passes) only match opening/self-closing forms. The orphan closing tag therefore reaches the TTS as literal text on the audio path (see inference/tts.ts, which calls markup.convert(markup.normalize(token)) before sending input_transcript) and can be spoken. Consider having normalizeMarkup also drop the now-orphaned closing tag when it converts an opening tag to self-closing, or add a final sweep in convertMarkup that removes any remaining unmatched native closing tags for the provider's tag list — the same defensive stance convertExpr already takes for `</expr>`.
Open in Devin Review

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

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.

2 participants