mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-30 19:32:27 +00:00
113 lines
2.9 KiB
TypeScript
113 lines
2.9 KiB
TypeScript
export const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
|
|
|
|
export const OPENAI_TTS_MODELS = ["gpt-4o-mini-tts", "tts-1", "tts-1-hd"] as const;
|
|
|
|
export const OPENAI_TTS_VOICES = [
|
|
"alloy",
|
|
"ash",
|
|
"ballad",
|
|
"cedar",
|
|
"coral",
|
|
"echo",
|
|
"fable",
|
|
"juniper",
|
|
"marin",
|
|
"onyx",
|
|
"nova",
|
|
"sage",
|
|
"shimmer",
|
|
"verse",
|
|
] as const;
|
|
|
|
type OpenAiTtsVoice = (typeof OPENAI_TTS_VOICES)[number];
|
|
|
|
export function normalizeOpenAITtsBaseUrl(baseUrl?: string): string {
|
|
const trimmed = baseUrl?.trim();
|
|
if (!trimmed) {
|
|
return DEFAULT_OPENAI_BASE_URL;
|
|
}
|
|
return trimmed.replace(/\/+$/, "");
|
|
}
|
|
|
|
function isCustomOpenAIEndpoint(baseUrl?: string): boolean {
|
|
if (baseUrl != null) {
|
|
return normalizeOpenAITtsBaseUrl(baseUrl) !== DEFAULT_OPENAI_BASE_URL;
|
|
}
|
|
return normalizeOpenAITtsBaseUrl(process.env.OPENAI_TTS_BASE_URL) !== DEFAULT_OPENAI_BASE_URL;
|
|
}
|
|
|
|
export function isValidOpenAIModel(model: string, baseUrl?: string): boolean {
|
|
if (isCustomOpenAIEndpoint(baseUrl)) {
|
|
return true;
|
|
}
|
|
return OPENAI_TTS_MODELS.includes(model as (typeof OPENAI_TTS_MODELS)[number]);
|
|
}
|
|
|
|
export function isValidOpenAIVoice(voice: string, baseUrl?: string): voice is OpenAiTtsVoice {
|
|
if (isCustomOpenAIEndpoint(baseUrl)) {
|
|
return true;
|
|
}
|
|
return OPENAI_TTS_VOICES.includes(voice as OpenAiTtsVoice);
|
|
}
|
|
|
|
export function resolveOpenAITtsInstructions(
|
|
model: string,
|
|
instructions?: string,
|
|
): string | undefined {
|
|
const next = instructions?.trim();
|
|
return next && model.includes("gpt-4o-mini-tts") ? next : undefined;
|
|
}
|
|
|
|
export async function openaiTTS(params: {
|
|
text: string;
|
|
apiKey: string;
|
|
baseUrl: string;
|
|
model: string;
|
|
voice: string;
|
|
speed?: number;
|
|
instructions?: string;
|
|
responseFormat: "mp3" | "opus" | "pcm";
|
|
timeoutMs: number;
|
|
}): Promise<Buffer> {
|
|
const { text, apiKey, baseUrl, model, voice, speed, instructions, responseFormat, timeoutMs } =
|
|
params;
|
|
const effectiveInstructions = resolveOpenAITtsInstructions(model, instructions);
|
|
|
|
if (!isValidOpenAIModel(model, baseUrl)) {
|
|
throw new Error(`Invalid model: ${model}`);
|
|
}
|
|
if (!isValidOpenAIVoice(voice, baseUrl)) {
|
|
throw new Error(`Invalid voice: ${voice}`);
|
|
}
|
|
|
|
const controller = new AbortController();
|
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
|
|
try {
|
|
const response = await fetch(`${baseUrl}/audio/speech`, {
|
|
method: "POST",
|
|
headers: {
|
|
Authorization: `Bearer ${apiKey}`,
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
model,
|
|
input: text,
|
|
voice,
|
|
response_format: responseFormat,
|
|
...(speed != null && { speed }),
|
|
...(effectiveInstructions != null && { instructions: effectiveInstructions }),
|
|
}),
|
|
signal: controller.signal,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`OpenAI TTS API error (${response.status})`);
|
|
}
|
|
|
|
return Buffer.from(await response.arrayBuffer());
|
|
} finally {
|
|
clearTimeout(timeout);
|
|
}
|
|
}
|