Files
Bruno ClermontandBruno Clermont 0184678dc3 fix(tts): validate voice compatibility with selected provider before synthesis (#1302)
An agent's tts_voice_id (e.g. "en-US-GuyNeural" for Edge) was applied
regardless of the selected TTS provider, causing OpenAI and other
providers to reject unknown voice IDs.

- Add internal/audio/voice_compat.go with IsVoiceCompatible,
  GetProviderDefaultVoice, and FilterVoiceForProvider helpers
- Update TtsTool.resolveVoiceAndModel to report whether the voice
  came from an agent override (voiceFromAgent)
- Apply applyVoiceCompat per-provider branch in Execute so
  incompatible agent voices fall back to the provider's default
  with a slog.Warn for operator visibility
- Add voice_compat_test.go with table-driven tests for all cases

Co-authored-by: Bruno Clermont <bruno.clermont@gmail.com>
2026-06-29 06:54:45 +07:00

61 lines
1.9 KiB
Go

package audio
import "strings"
// edgeVoiceDefaultVoice is the default Edge TTS voice.
const edgeVoiceDefaultVoice = "en-US-AriaNeural"
// openaiDefaultVoice is the default OpenAI TTS voice.
const openaiDefaultVoice = "alloy"
// IsVoiceCompatible reports whether the given voice ID is compatible with the
// named TTS provider. Returns true for providers without validation rules.
//
// Edge voices follow the BCP-47 + Neural suffix pattern (e.g. "en-US-GuyNeural").
// OpenAI voices are a fixed set: alloy, echo, fable, onyx, nova, shimmer.
func IsVoiceCompatible(provider, voice string) bool {
if voice == "" {
return true
}
switch provider {
case "edge":
return strings.Contains(voice, "Neural")
case "openai":
switch voice {
case "alloy", "echo", "fable", "onyx", "nova", "shimmer":
return true
}
return false
default:
// No validation for other providers.
return true
}
}
// GetProviderDefaultVoice returns the default voice ID for the named provider.
// Returns an empty string for providers where the SDK selects its own default.
func GetProviderDefaultVoice(provider string) string {
switch provider {
case "edge":
return edgeVoiceDefaultVoice
case "openai":
return openaiDefaultVoice
default:
return ""
}
}
// FilterVoiceForProvider returns the voice to use for the given provider.
// If the voice is incompatible with the provider it falls back to the
// provider's default voice (which may be empty, signalling "use SDK default").
// agentOverride indicates the voice came from agent configuration (not from
// explicit tool args or tenant defaults) so that the caller can decide whether
// to emit a warning.
func FilterVoiceForProvider(provider, voice string, agentOverride bool) (filtered string, changed bool) {
if IsVoiceCompatible(provider, voice) {
return voice, false
}
def := GetProviderDefaultVoice(provider)
return def, true
}