feat(tts): add Fish Audio expressive markup - #2190
feat(tts): add Fish Audio expressive markup#2190rosetta-livekit-bot[bot] wants to merge 1 commit into
Conversation
🦋 Changeset detectedLatest commit: de2ab8b The changes in this PR will be included in the next version bump. This PR includes changesets to release 36 packages
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 |
| // Fish's native dialect is square brackets, produced only for the TTS. These | ||
| // names catch hallucinated XML-native markup in transcripts. | ||
| fishaudio: [FISHAUDIO_TAGS, false], |
There was a problem hiding this comment.
🟡 Expressive speech markers are turned on for every Fish Audio voice, not only the models that understand them
Expressive markup is enabled for any Fish Audio voice by keying only on the provider prefix (_markupProviderKey() at agents/src/inference/tts.ts:387-395), even though the new markers are only understood by the s2 family, so older Fish voices get emotion/sound/pause markers they cannot interpret.
Impact: A user running an older Fish Audio voice with expressive mode can hear bracketed marker text read aloud or get garbled delivery.
Provider-key derivation vs. the s2-only marker vocabulary
PROVIDER_MARKUP/llmInstructions gain a fishaudio entry (agents/src/tts/_provider_format.ts:983-1001), and the vocabulary is explicitly documented as the "Fish Audio (s2 family) speech markers" (agents/src/tts/_provider_format.ts:120). But InferenceTTS._markupProviderKey() returns the raw provider segment of the model string for anything other than Inworld, which already has exactly this kind of gate (return model.includes('tts-2') ? 'inworld' : ''). Consequently fishaudio/s1, fishaudio/s1-mini, etc. also resolve to 'fishaudio', so _resolveExpressiveOptions() (agents/src/voice/agent_activity.ts:1917) sees markup support, the LLM is instructed to emit <expr .../> markers, and convertMarkup lowers them to [very excited], [laughing], [break], [emphasis] before they are sent to a model that does not implement them.
A model gate mirroring the Inworld one (only s2 models return 'fishaudio') would restore the intended scope.
Prompt for agents
The Fish Audio markup vocabulary added in agents/src/tts/_provider_format.ts is documented as s2-family only (see the FISHAUDIO_* tables and the doc comment "Fish Audio (s2 family) speech markers"), and the PR intent is to add Fish Audio s2 models to expressive mode. However, the provider key used to look up these tables comes from InferenceTTS._markupProviderKey() in agents/src/inference/tts.ts, which simply returns the provider segment of the gateway model string for every provider except Inworld. Inworld already has a model-level gate there (only models containing 'tts-2' return 'inworld', older ones return '' so expressive stays off). No equivalent gate exists for Fish, so any fishaudio/* model (including non-s2 voices) is treated as markup-capable: _resolveExpressiveOptions() in agents/src/voice/agent_activity.ts enables expressive, the LLM is taught the expr dialect, and convertMarkup emits [very excited]/[laughing]/[break]/[emphasis] to a model that may not support them. Add a Fish model gate alongside the Inworld one so only the s2 family maps to the 'fishaudio' markup key, and consider adding the supported Fish model strings to the inference TTS model unions for discoverability.
Was this helpful? React with 👍 or 👎 to provide feedback.
Ports livekit/agents#6631 to add Fish Audio s2 models to expressive mode. Fish now receives native emotion, sound, break, and emphasis brackets, with steering-aware sound/example filtering and clean transcript handling.
Adds the minimal speech-steering infrastructure the JS target lacked, including public steering types and queryable markup capabilities.
Verification
pnpm vitest run agents/src/tts/expr_markup.test.ts agents/src/tts/markup_utils.test.tspnpm vitest run agents(1284 passed, 2 skipped; 11 pre-existing AMD failures remain, plus one EOT retry timing failure that passed in isolation)pnpm vitest run agents/src/inference/eot/transports.test.tspnpm --filter @livekit/agents buildpnpm buildpnpm --filter @livekit/agents lint(passes with existing warnings)cue-clivoice run withfishaudio/s2.1-pro: emitted excited/laughing markers, synthesized Fish audio, and completed without framework errorspnpm --filter @livekit/agents api:checkattempted; blocked by API Extractor not supporting the repository's existingexport * assyntaxSource diff coverage
livekit-agents/livekit/agents/tts/_provider_format.py->agents/src/tts/_provider_format.ts. Ported the Fish s2 emotion/sound vocabulary, instruction examples, English-only labels, steering filters, capability matrix, aliases, expression intensification, break-duration mapping, emphasis conversion, transcript registration, normalization, and intentional per-sentence chunking behavior. Added the target-missing steering propagation inagents/src/tts/tts.ts,agents/src/voice/agent_session.ts,agents/src/voice/agent_activity.ts,agents/src/voice/presets.ts, and public exports.tests/test_tokenizer_xml_markup.py->agents/src/tts/expr_markup.test.ts. Ported all 10 Fish Audio test cases to Vitest without adding source-absent behavioral tests.Source: livekit/agents#6631
Ported from livekit/agents#6631
Original PR description
rebase of #6528 onto main (the original was auto-closed when tina/refactor-expressive-mode merged in #6406).
adds fish audio (s2 models) to expressive mode, same shape as inworld/xai/cartesia:
[very EMOTION][break]/[long-break]by durationare you [emphasis] sure[laughing],[chuckling],[clear throat], governed by thelaughing/reflex_soundssteering fields; disabled sounds are dropped from the advertised vocabulary and examples like the other providersdisfluenciesis enabled, so examples never contradict the delivery guidelinesno
_MAX_INPUT_LENentry on purpose: fish's markers are sentence-scoped (every sentence carries its own emotion tag), so we stick with normal per-sentence synthesis instead of batching longer expressive chunks — no steering is lost and time-to-first-audio stays low.adapted to the post-#6406 architecture during the rebase: fish registers in the new
_PROVIDER_MARKUP(plain tag-name list), and transcript tests usesplit_all_markup— bracket cues are handled at the aligned-transcript source bydrop_bracket_cues, matching the other providers.verified live through the inference gateway (
fishaudio/s2.1-pro+ gemma-4-31b-it) and with offline sweeps across several llms checking tag placement, conversion, transcript stripping, and steering filters. 1624 unit tests + strict mypy green on the rebased stack.