// Gradium provider module implements model/runtime integration. import { resolveGeneratedMediaMaxBytes } from "openclaw/plugin-sdk/media-generation-runtime"; import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input"; import type { SpeechDirectiveTokenParseContext, SpeechProviderConfig, SpeechProviderPlugin, SpeechSynthesisRequest, SpeechTelephonySynthesisRequest, } from "openclaw/plugin-sdk/speech"; import { trimToUndefined } from "openclaw/plugin-sdk/speech"; import { resolveSpeechProviderApiKey } from "openclaw/plugin-sdk/speech-core"; import { asOptionalRecord } from "openclaw/plugin-sdk/string-coerce-runtime"; import { DEFAULT_GRADIUM_VOICE_ID, GRADIUM_VOICES, normalizeGradiumBaseUrl } from "./shared.js"; import { gradiumTTS } from "./tts.js"; type GradiumProviderConfig = { apiKey?: string; baseUrl: string; voiceId: string; }; function normalizeGradiumProviderConfig(rawConfig: Record): GradiumProviderConfig { const providers = asOptionalRecord(rawConfig.providers); const raw = asOptionalRecord(providers?.gradium) ?? asOptionalRecord(rawConfig.gradium); return { apiKey: normalizeResolvedSecretInputString({ value: raw?.apiKey, path: "tts.providers.gradium.apiKey", }), baseUrl: normalizeGradiumBaseUrl(trimToUndefined(raw?.baseUrl)), voiceId: trimToUndefined(raw?.voiceId) ?? DEFAULT_GRADIUM_VOICE_ID, }; } function readGradiumProviderConfig(config: SpeechProviderConfig): GradiumProviderConfig { const defaults = normalizeGradiumProviderConfig({}); return { apiKey: trimToUndefined(config.apiKey) ?? defaults.apiKey, baseUrl: normalizeGradiumBaseUrl(trimToUndefined(config.baseUrl) ?? defaults.baseUrl), voiceId: trimToUndefined(config.voiceId) ?? defaults.voiceId, }; } function resolveGradiumApiKey(configApiKey: unknown): string | undefined { return resolveSpeechProviderApiKey(trimToUndefined(configApiKey), process.env.GRADIUM_API_KEY); } async function synthesizeGradium( req: SpeechSynthesisRequest | SpeechTelephonySynthesisRequest, outputFormat: "wav" | "opus" | "ulaw_8000", ): Promise { const config = readGradiumProviderConfig(req.providerConfig); const apiKey = resolveGradiumApiKey(config.apiKey); if (!apiKey) { throw new Error("Gradium API key missing"); } return await gradiumTTS({ text: req.text, apiKey, baseUrl: config.baseUrl, voiceId: trimToUndefined(req.providerOverrides?.voiceId) ?? config.voiceId, outputFormat, timeoutMs: req.timeoutMs, maxBytes: resolveGeneratedMediaMaxBytes(req.cfg, "audio"), }); } function isGradiumProviderConfigured(config: SpeechProviderConfig): boolean { const apiKey = resolveGradiumApiKey(config.apiKey); if (!apiKey) { return false; } try { normalizeGradiumBaseUrl(trimToUndefined(config.baseUrl)); return true; } catch { // Provider selection is a predicate; synthesis reports the precise URL error. return false; } } function parseDirectiveToken(ctx: SpeechDirectiveTokenParseContext): { handled: boolean; overrides?: Record; warnings?: string[]; } { switch (ctx.key) { case "voice": case "voice_id": case "voiceid": case "gradium_voice": case "gradiumvoice": if (!ctx.policy.allowVoice) { return { handled: true }; } return { handled: true, overrides: { ...ctx.currentOverrides, voiceId: ctx.value }, }; default: return { handled: false }; } } export function buildGradiumSpeechProvider(): SpeechProviderPlugin { return { id: "gradium", label: "Gradium", autoSelectOrder: 30, voices: GRADIUM_VOICES.map((v) => v.id), resolveConfig: ({ rawConfig }) => normalizeGradiumProviderConfig(rawConfig), parseDirectiveToken, listVoices: async () => GRADIUM_VOICES.map((v) => ({ id: v.id, name: v.name })), isConfigured: ({ providerConfig }) => isGradiumProviderConfigured(providerConfig), synthesize: async (req) => { const wantsVoiceNote = req.target === "voice-note"; const outputFormat = wantsVoiceNote ? "opus" : "wav"; const audioBuffer = await synthesizeGradium(req, outputFormat); return { audioBuffer, outputFormat, fileExtension: wantsVoiceNote ? ".opus" : ".wav", voiceCompatible: wantsVoiceNote, }; }, synthesizeTelephony: async (req) => { const outputFormat = "ulaw_8000"; const sampleRate = 8_000; const audioBuffer = await synthesizeGradium(req, outputFormat); return { audioBuffer, outputFormat, sampleRate }; }, }; }