Files
infinite-canvas/web/src/services/api/audio.ts
T

118 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import axios from "axios";
import { audioMimeType, normalizeAudioFormatValue, normalizeAudioSpeedValue, normalizeAudioVoiceValue } from "@/lib/audio-generation";
import { uploadMediaFile, type UploadedFile } from "@/services/file-storage";
import { buildApiUrl, resolveModelRequestConfig, resolveModelScript, type AiConfig } from "@/stores/use-config-store";
import { runModelPlugin } from "./model-plugin";
type RequestOptions = { signal?: AbortSignal };
function aiApiUrl(config: AiConfig, path: string) {
return buildApiUrl(config.baseUrl, path);
}
function aiHeaders(config: AiConfig) {
return {
Authorization: `Bearer ${config.apiKey}`,
"Content-Type": "application/json",
};
}
export async function requestAudioGeneration(config: AiConfig, prompt: string, options?: RequestOptions): Promise<Blob> {
const requestConfig = resolveModelRequestConfig(config, config.model || config.audioModel);
const model = requestConfig.model.trim();
const format = normalizeAudioFormatValue(config.audioFormat);
const script = resolveModelScript(config, config.model || config.audioModel);
if (script) {
if (!model) throw new Error("请先配置音频模型");
if (!requestConfig.baseUrl.trim()) throw new Error("请先配置 Base URL");
if (!requestConfig.apiKey.trim()) throw new Error("请先配置 API Key");
try {
const result = await runModelPlugin({
capability: "audio",
script,
config: requestConfig,
input: { prompt, params: { voice: normalizeAudioVoiceValue(config.audioVoice), format, speed: normalizeAudioSpeedValue(config.audioSpeed), instructions: config.audioInstructions.trim() } },
signal: options?.signal,
});
return await audioPluginBlob(result, format);
} catch (error) {
throw new Error(readAxiosError(error, "音频生成失败"));
}
}
assertAudioConfig(requestConfig, model);
const instructions = config.audioInstructions.trim();
try {
const response = await axios.post<Blob>(
aiApiUrl(requestConfig, "/audio/speech"),
{
model,
input: prompt,
voice: normalizeAudioVoiceValue(config.audioVoice),
response_format: format,
speed: Number(normalizeAudioSpeedValue(config.audioSpeed)),
...(instructions ? { instructions } : {}),
},
{ headers: aiHeaders(requestConfig), responseType: "blob", signal: options?.signal },
);
await assertAudioBlob(response.data);
return response.data.type.startsWith("audio/") ? response.data : new Blob([response.data], { type: audioMimeType(format) });
} catch (error) {
throw new Error(readAxiosError(error, "音频生成失败"));
}
}
async function audioPluginBlob(result: unknown, format: string): Promise<Blob> {
if (result instanceof Blob) return result.type.startsWith("audio/") ? result : new Blob([result], { type: audioMimeType(format) });
let source = "";
if (typeof result === "string") source = result;
else if (result && typeof result === "object") {
const record = result as Record<string, unknown>;
source = typeof record.b64_json === "string" ? record.b64_json : typeof record.data === "string" ? record.data : typeof record.url === "string" ? record.url : "";
}
if (!source) throw new Error("模型调用脚本没有返回音频");
const url = source.startsWith("data:") || /^https?:/i.test(source) ? source : `data:${audioMimeType(format)};base64,${source}`;
const blob = await (await fetch(url)).blob();
return blob.type.startsWith("audio/") ? blob : new Blob([blob], { type: audioMimeType(format) });
}
export async function storeGeneratedAudio(blob: Blob, format = "mp3"): Promise<UploadedFile> {
const audio = blob.type.startsWith("audio/") ? blob : new Blob([blob], { type: audioMimeType(format) });
return uploadMediaFile(audio, "audio");
}
function assertAudioConfig(config: AiConfig, model: string) {
if (!model) throw new Error("请先配置音频模型");
if (!config.baseUrl.trim()) throw new Error("请先配置 Base URL");
if (!config.apiKey.trim()) throw new Error("请先配置 API Key");
if (config.apiFormat === "gemini") throw new Error("Gemini 调用格式暂不支持音频生成,请使用 OpenAI 格式渠道");
}
async function assertAudioBlob(blob: Blob) {
if (!blob.type.includes("json")) return;
let payload: { code?: number; msg?: string; error?: { message?: string } };
try {
payload = JSON.parse(await blob.text()) as { code?: number; msg?: string; error?: { message?: string } };
} catch {
return;
}
if (typeof payload.code === "number" && payload.code !== 0) throw new Error(payload.msg || "音频生成失败");
if (payload.error?.message) throw new Error(payload.error.message);
}
function readAxiosError(error: unknown, fallback: string) {
if (axios.isCancel(error)) return "请求已取消";
if (axios.isAxiosError<{ error?: { message?: string }; msg?: string; code?: number }>(error)) {
const responseData = error.response?.data;
return responseData?.msg || responseData?.error?.message || statusMessage(error.response?.status, fallback);
}
return error instanceof Error ? error.message : fallback;
}
function statusMessage(status: number | undefined, fallback: string) {
if (status === 401 || status === 403) return "鉴权失败,请检查 API Key、套餐权限或模型权限";
if (status === 429) return "请求被限流或额度不足,请稍后重试";
return status ? `${fallback}${status}` : fallback;
}