feat(tts): dedicated ATIS broadcast voice on Speaches

The ATIS tag now resolves to en_GB-alba-medium — outside both voice pools —
instead of sharing 'verse' with the user's own readback voice.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
itsrubberduck
2026-07-19 11:22:44 +02:00
parent a60942bfd8
commit 1272d45bf9
3 changed files with 21 additions and 2 deletions

View File

@@ -212,7 +212,7 @@ export default defineEventHandler(async (event) => {
voice,
};
const speachesVoice = provider === "speaches"
? resolveSpeachesVoice(voice, speachesFallback)
? resolveSpeachesVoice(voice, speachesFallback, body?.tag)
: null;
const ttsVoice = speachesVoice?.voice ?? voice;
const providerModel = speachesVoice

View File

@@ -28,6 +28,14 @@ describe('resolveSpeachesVoice', () => {
assert.deepEqual(resolveSpeachesVoice('', fallback), fallback)
})
it('gives the ATIS broadcast its own dedicated speaker', () => {
const atis = resolveSpeachesVoice('verse', fallback, 'atis')
assert.match(atis.voice, /^en_/)
assert.notEqual(atis.voice, resolveSpeachesVoice('verse', fallback).voice)
const allPoolVoices = Object.values(SPEACHES_VOICE_MAP).map(v => v.voice)
assert.equal(allPoolVoices.includes(atis.voice), false, 'ATIS voice must not be in any pool')
})
it('keeps controller and pilot voices audibly disjoint', () => {
const controller = ['alloy', 'echo', 'onyx', 'sage'].map(v => resolveSpeachesVoice(v, fallback).voice)
const pilots = ['verse', 'ash', 'ballad', 'coral', 'fable', 'nova', 'shimmer']

View File

@@ -34,7 +34,18 @@ export const SPEACHES_VOICE_MAP: Record<string, SpeachesVoice> = {
shimmer: piper('en_US-kristin-medium'),
}
export function resolveSpeachesVoice(logical: string, fallback: SpeachesVoice): SpeachesVoice {
/**
* ATIS is a robotic broadcast in the real world — it gets a dedicated speaker
* outside every pool so it never sounds like a controller or a pilot.
*/
export const ATIS_SPEACHES_VOICE: SpeachesVoice = piper('en_GB-alba-medium')
export function resolveSpeachesVoice(
logical: string,
fallback: SpeachesVoice,
tag?: string,
): SpeachesVoice {
if ((tag || '').trim().toLowerCase() === 'atis') return ATIS_SPEACHES_VOICE
const key = (logical || '').trim().toLowerCase()
return SPEACHES_VOICE_MAP[key] ?? fallback
}