import axios from "axios"; import { audioMimeType, normalizeAudioFormatValue, normalizeAudioSpeedValue, normalizeAudioVoiceValue } from "@/lib/audio-generation"; import { uploadMediaFile, type UploadedFile } from "@/services/file-storage"; import { buildApiUrl, type AiConfig } from "@/stores/use-config-store"; import { useUserStore } from "@/stores/use-user-store"; function aiApiUrl(config: AiConfig, path: string) { return config.channelMode === "remote" ? `/api/v1${path}` : buildApiUrl(config.baseUrl, path); } function aiHeaders(config: AiConfig) { const token = useUserStore.getState().token; return config.channelMode === "remote" ? { ...(token ? { Authorization: `Bearer ${token}` } : {}), "Content-Type": "application/json", } : { Authorization: `Bearer ${config.apiKey}`, "Content-Type": "application/json", }; } function refreshRemoteUser(config: AiConfig) { if (config.channelMode === "remote") void useUserStore.getState().hydrateUser(); } export async function requestAudioGeneration(config: AiConfig, prompt: string): Promise { const model = (config.model || config.audioModel).trim(); assertAudioConfig(config, model); const format = normalizeAudioFormatValue(config.audioFormat); const instructions = config.audioInstructions.trim(); try { const response = await axios.post( aiApiUrl(config, "/audio/speech"), { model, input: prompt, voice: normalizeAudioVoiceValue(config.audioVoice), response_format: format, speed: Number(normalizeAudioSpeedValue(config.audioSpeed)), ...(instructions ? { instructions } : {}), }, { headers: aiHeaders(config), responseType: "blob" }, ); await assertAudioBlob(response.data); refreshRemoteUser(config); return response.data.type.startsWith("audio/") ? response.data : new Blob([response.data], { type: audioMimeType(format) }); } catch (error) { throw new Error(readAxiosError(error, "音频生成失败")); } } export async function storeGeneratedAudio(blob: Blob, format = "mp3"): Promise { 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.channelMode === "local" && !config.baseUrl.trim()) throw new Error("请先配置 Base URL"); if (config.channelMode === "local" && !config.apiKey.trim()) throw new Error("请先配置 API Key"); } 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.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; }