ElevenLabs speaks for the voice tool when ELEVENLABS_API_KEY and ELEVENLABS_VOICE_ID are set (TTS_PROVIDER=elevenlabs|openai picks, the other is the fallback). Opus output (opus_48000_64) goes to Telegram as is; voice names resolve through /v2/voices; instructions become audio tags on eleven_v3 and are ignored on other models. The key is redacted from tool output and never reaches agent processes.
118 lines
5.8 KiB
C#
118 lines
5.8 KiB
C#
namespace Volcanic.AgenticCore.Telegram.Speech;
|
|
|
|
/// <summary>Which service speaks for the <c>voice</c> tool.</summary>
|
|
public enum TextToSpeechProvider
|
|
{
|
|
None,
|
|
OpenAi,
|
|
ElevenLabs,
|
|
}
|
|
|
|
/// <summary>
|
|
/// The <c>Telegram:Speech</c> section. Speech to text (voice notes in) is on exactly when <see cref="ApiKey"/> is set;
|
|
/// text to speech (the <c>voice</c> tool) uses <see cref="ElevenLabs"/> when it is configured, else OpenAI with the same key.
|
|
/// </summary>
|
|
public sealed class SpeechOptions
|
|
{
|
|
public const string DefaultModel = "gpt-4o-transcribe";
|
|
public const string DefaultBaseUrl = "https://api.openai.com/v1";
|
|
public const string DefaultTtsModel = "gpt-4o-mini-tts";
|
|
public const string DefaultTtsVoice = "nova";
|
|
|
|
/// <summary>Falls back to <c>OPENAI_API_KEY</c>. Empty turns speech to text off.</summary>
|
|
public string ApiKey { get; set; } = "";
|
|
|
|
/// <summary>Falls back to <c>STT_MODEL</c>, then <see cref="DefaultModel"/>.</summary>
|
|
public string Model { get; set; } = "";
|
|
|
|
/// <summary>Falls back to <c>TTS_MODEL</c>, then <see cref="DefaultTtsModel"/>.</summary>
|
|
public string TtsModel { get; set; } = "";
|
|
|
|
/// <summary>Falls back to <c>TTS_VOICE</c>, then <see cref="DefaultTtsVoice"/>. The tool's <c>voice</c> argument overrides it.</summary>
|
|
public string TtsVoice { get; set; } = "";
|
|
|
|
/// <summary>Default delivery instructions (tone, pace); the tool's <c>instructions</c> argument overrides them. Ignored by tts-1.</summary>
|
|
public string? TtsInstructions { get; set; }
|
|
|
|
/// <summary>
|
|
/// Falls back to <c>TTS_PROVIDER</c>: <c>elevenlabs</c> or <c>openai</c>. Empty prefers ElevenLabs. The preferred
|
|
/// provider is used when configured, else the other one.
|
|
/// </summary>
|
|
public string TtsProvider { get; set; } = "";
|
|
|
|
/// <summary>ElevenLabs text to speech (<c>Telegram:Speech:ElevenLabs</c>).</summary>
|
|
public ElevenLabsOptions ElevenLabs { get; set; } = new();
|
|
|
|
/// <summary>Any OpenAI-compatible audio endpoint (transcriptions and speech).</summary>
|
|
public string BaseUrl { get; set; } = DefaultBaseUrl;
|
|
|
|
/// <summary>Optional ISO-639-1 hint.</summary>
|
|
public string? Language { get; set; }
|
|
|
|
/// <summary>Tries per transcription; network errors, timeouts, 408, 429 and 5xx are retried.</summary>
|
|
public const int MaxAttempts = 3;
|
|
|
|
/// <summary>The wait before the second attempt, doubled before the third. Internal so tests can zero it.</summary>
|
|
internal TimeSpan RetryDelay { get; set; } = TimeSpan.FromSeconds(1);
|
|
|
|
public bool IsEnabled => !string.IsNullOrWhiteSpace(ApiKey);
|
|
|
|
public string ResolvedModel => string.IsNullOrWhiteSpace(Model) ? DefaultModel : Model.Trim();
|
|
|
|
public string ResolvedTtsModel => string.IsNullOrWhiteSpace(TtsModel) ? DefaultTtsModel : TtsModel.Trim();
|
|
|
|
public string ResolvedTtsVoice => string.IsNullOrWhiteSpace(TtsVoice) ? DefaultTtsVoice : TtsVoice.Trim();
|
|
|
|
/// <summary>The service behind the <c>voice</c> tool, or <see cref="TextToSpeechProvider.None"/> when neither is configured.</summary>
|
|
public TextToSpeechProvider ResolvedTtsProvider
|
|
{
|
|
get
|
|
{
|
|
var elevenLabs = ElevenLabs?.IsConfigured == true;
|
|
var preferOpenAi = string.Equals(TtsProvider?.Trim(), "openai", StringComparison.OrdinalIgnoreCase);
|
|
return (preferOpenAi, IsEnabled, elevenLabs) switch
|
|
{
|
|
(true, true, _) => TextToSpeechProvider.OpenAi,
|
|
(false, _, true) => TextToSpeechProvider.ElevenLabs,
|
|
(_, true, _) => TextToSpeechProvider.OpenAi,
|
|
(_, _, true) => TextToSpeechProvider.ElevenLabs,
|
|
_ => TextToSpeechProvider.None,
|
|
};
|
|
}
|
|
}
|
|
|
|
/// <summary>Settings problems that must stop the host, added to <paramref name="errors"/>.</summary>
|
|
public void Validate(List<string> errors)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(errors);
|
|
// On Linux "/v1" parses as an absolute file:// URI; the scheme check rejects it.
|
|
if (!Uri.TryCreate(BaseUrl, UriKind.Absolute, out var sttUri) || sttUri.Scheme is not ("http" or "https"))
|
|
errors.Add("Telegram:Speech:BaseUrl must be an absolute http(s) URL.");
|
|
if (!string.IsNullOrWhiteSpace(TtsProvider) && TtsProvider.Trim().ToLowerInvariant() is not ("openai" or "elevenlabs"))
|
|
errors.Add("TTS_PROVIDER (Telegram:Speech:TtsProvider) must be 'elevenlabs' or 'openai'.");
|
|
if (!Uri.TryCreate(ElevenLabs?.BaseUrl, UriKind.Absolute, out var elevenUri) || elevenUri.Scheme is not ("http" or "https"))
|
|
errors.Add("Telegram:Speech:ElevenLabs:BaseUrl must be an absolute http(s) URL.");
|
|
}
|
|
|
|
/// <summary>The startup log line (never a key).</summary>
|
|
public string Describe()
|
|
{
|
|
var provider = ResolvedTtsProvider;
|
|
var stt = IsEnabled ? $"Speech to text: {ResolvedModel}" : "Speech to text: off (no OPENAI_API_KEY)";
|
|
var tts = provider switch
|
|
{
|
|
TextToSpeechProvider.OpenAi => $"text to speech: {ResolvedTtsModel}, voice {ResolvedTtsVoice}",
|
|
TextToSpeechProvider.ElevenLabs => $"text to speech: ElevenLabs {ElevenLabs.ResolvedModel}, voice {ElevenLabs.VoiceId.Trim()}",
|
|
_ => "text to speech: off",
|
|
};
|
|
if (!IsEnabled && provider == TextToSpeechProvider.None) tts = "text to speech: off (no OPENAI_API_KEY or ELEVENLABS_API_KEY)";
|
|
var line = $"{stt}; {tts}";
|
|
if (provider != TextToSpeechProvider.ElevenLabs && ElevenLabs is { } eleven
|
|
&& string.IsNullOrWhiteSpace(eleven.ApiKey) != string.IsNullOrWhiteSpace(eleven.VoiceId))
|
|
line += string.IsNullOrWhiteSpace(eleven.VoiceId)
|
|
? " (ElevenLabs off: ELEVENLABS_VOICE_ID is not set)"
|
|
: " (ElevenLabs off: ELEVENLABS_API_KEY is not set)";
|
|
return line;
|
|
}
|
|
}
|