diff --git a/extensions/minimax/index.ts b/extensions/minimax/index.ts index 61729c23eb8..c403d1f2f74 100644 --- a/extensions/minimax/index.ts +++ b/extensions/minimax/index.ts @@ -23,7 +23,11 @@ import { } from "./media-understanding-provider.js"; import type { MiniMaxRegion } from "./oauth.js"; import { applyMinimaxApiConfig, applyMinimaxApiConfigCn } from "./onboard.js"; -import { buildMinimaxPortalProvider, buildMinimaxProvider } from "./provider-catalog.js"; +import { + buildMinimaxPortalProvider, + buildMinimaxProvider, +} from "./provider-catalog.js"; +import { buildMinimaxSpeechProvider } from "./speech-provider.js"; const API_PROVIDER_ID = "minimax"; const PORTAL_PROVIDER_ID = "minimax-portal"; @@ -44,7 +48,10 @@ function portalModelRef(modelId: string): string { return `${PORTAL_PROVIDER_ID}/${modelId}`; } -function buildPortalProviderCatalog(params: { baseUrl: string; apiKey: string }) { +function buildPortalProviderCatalog(params: { + baseUrl: string; + apiKey: string; +}) { return { ...buildMinimaxPortalProvider(), baseUrl: params.baseUrl, @@ -71,16 +78,24 @@ function resolvePortalCatalog(ctx: ProviderCatalogContext) { const authStore = ensureAuthProfileStore(ctx.agentDir, { allowKeychainPrompt: false, }); - const hasProfiles = listProfilesForProvider(authStore, PORTAL_PROVIDER_ID).length > 0; + const hasProfiles = + listProfilesForProvider(authStore, PORTAL_PROVIDER_ID).length > 0; const explicitApiKey = - typeof explicitProvider?.apiKey === "string" ? explicitProvider.apiKey.trim() : undefined; - const apiKey = envApiKey ?? explicitApiKey ?? (hasProfiles ? MINIMAX_OAUTH_MARKER : undefined); + typeof explicitProvider?.apiKey === "string" + ? explicitProvider.apiKey.trim() + : undefined; + const apiKey = + envApiKey ?? + explicitApiKey ?? + (hasProfiles ? MINIMAX_OAUTH_MARKER : undefined); if (!apiKey) { return null; } const explicitBaseUrl = - typeof explicitProvider?.baseUrl === "string" ? explicitProvider.baseUrl.trim() : undefined; + typeof explicitProvider?.baseUrl === "string" + ? explicitProvider.baseUrl.trim() + : undefined; return { provider: buildPortalProviderCatalog({ @@ -95,7 +110,9 @@ function createOAuthHandler(region: MiniMaxRegion) { const regionLabel = region === "cn" ? "CN" : "Global"; return async (ctx: ProviderAuthContext): Promise => { - const progress = ctx.prompter.progress(`Starting MiniMax OAuth (${regionLabel})…`); + const progress = ctx.prompter.progress( + `Starting MiniMax OAuth (${regionLabel})…`, + ); try { const { loginMiniMaxPortalOAuth } = await import("./oauth.runtime.js"); const result = await loginMiniMaxPortalOAuth({ @@ -233,7 +250,9 @@ export default definePluginEntry({ }); api.registerMediaUnderstandingProvider(minimaxMediaUnderstandingProvider); - api.registerMediaUnderstandingProvider(minimaxPortalMediaUnderstandingProvider); + api.registerMediaUnderstandingProvider( + minimaxPortalMediaUnderstandingProvider, + ); api.registerProvider({ id: PORTAL_PROVIDER_ID, @@ -278,6 +297,9 @@ export default definePluginEntry({ isModernModelRef: ({ modelId }) => isMiniMaxModernModelId(modelId), }); api.registerImageGenerationProvider(buildMinimaxImageGenerationProvider()); - api.registerImageGenerationProvider(buildMinimaxPortalImageGenerationProvider()); + api.registerImageGenerationProvider( + buildMinimaxPortalImageGenerationProvider(), + ); + api.registerSpeechProvider(buildMinimaxSpeechProvider()); }, }); diff --git a/extensions/minimax/speech-provider.ts b/extensions/minimax/speech-provider.ts new file mode 100644 index 00000000000..2e122cbedeb --- /dev/null +++ b/extensions/minimax/speech-provider.ts @@ -0,0 +1,101 @@ +import type { + SpeechProviderConfig, + SpeechProviderPlugin, +} from "openclaw/plugin-sdk/speech-core"; + +const GLOBAL_TTS_URL = "https://api.minimax.io/v1/t2a_v2"; +const DEFAULT_MODEL = "speech-2.8-hd"; + +function text(value: unknown): string | undefined { + return typeof value === "string" && value.trim() ? value.trim() : undefined; +} + +function configValue( + config: SpeechProviderConfig, + key: string, +): string | undefined { + return text(config[key]); +} + +export function buildMinimaxSpeechProvider(): SpeechProviderPlugin { + return { + id: "minimax", + label: "MiniMax", + autoSelectOrder: 25, + models: [ + "speech-2.8-hd", + "speech-2.8-turbo", + "speech-2.6-hd", + "speech-2.6-turbo", + ], + resolveConfig: ({ rawConfig }) => { + const providers = rawConfig.providers; + const providerConfig = + typeof providers === "object" && providers !== null + ? (providers as Record).minimax + : undefined; + return ( + typeof providerConfig === "object" && providerConfig !== null + ? providerConfig + : {} + ) as SpeechProviderConfig; + }, + isConfigured: ({ providerConfig }) => + Boolean( + configValue(providerConfig, "apiKey") || process.env.MINIMAX_API_KEY, + ), + synthesize: async (req) => { + const apiKey = + configValue(req.providerConfig, "apiKey") || + process.env.MINIMAX_API_KEY; + if (!apiKey) throw new Error("MiniMax API key missing"); + const endpoint = + configValue(req.providerConfig, "baseUrl") || GLOBAL_TTS_URL; + const model = + configValue(req.providerOverrides ?? {}, "model") || + configValue(req.providerConfig, "model") || + DEFAULT_MODEL; + const voice = + configValue(req.providerOverrides ?? {}, "voice") || + configValue(req.providerConfig, "voice") || + "female-shaonv"; + const response = await fetch(endpoint, { + method: "POST", + headers: { + Authorization: `Bearer ${apiKey}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + model, + text: req.text, + stream: false, + output_format: "hex", + voice_setting: { voice_id: voice }, + }), + signal: AbortSignal.timeout(req.timeoutMs), + }); + if (!response.ok) + throw new Error(`MiniMax TTS request failed (${response.status})`); + const payload = (await response.json()) as { + data?: { audio?: string }; + base_resp?: { status_code?: number }; + }; + if ( + payload.base_resp?.status_code && + payload.base_resp.status_code !== 0 + ) { + throw new Error( + `MiniMax TTS request failed (${payload.base_resp.status_code})`, + ); + } + if (!payload.data?.audio) + throw new Error("MiniMax TTS response did not include audio"); + return { + audioBuffer: Buffer.from(payload.data.audio, "hex"), + outputFormat: "mp3", + fileExtension: ".mp3", + voiceCompatible: false, + }; + }, + }; +}