import "server-only"; import { mkdtemp, readFile, rm } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { getEnabledAiModelsWithProvider } from "@/lib/db/queries"; import { uploadFile } from "@/lib/storage/service"; import { downloadCanvasMedia } from "./download-media"; import { audioContract, audioCheckpointSchema, audioRequestSchema, buildAudioRequest, officialAudioBase, } from "./audio-contract"; import { ProviderTaskUncertainError } from "./video-task-lifecycle"; export async function listCanvasAudioModels() { return (await getEnabledAiModelsWithProvider()).filter(({ model, provider }) => { try { return ( provider.type === "alibaba" && !!audioContract(model.modelId) && !!officialAudioBase(provider.baseUrl, model.modelId) ); } catch { return false; } }); } export async function executeCanvasAudio( params: unknown, lifecycle: { resume?: unknown; assertActive: () => Promise; checkpoint: (value: Record) => Promise; }, ) { const input = audioRequestSchema.parse(params); let checkpoint = lifecycle.resume ? audioCheckpointSchema.parse(lifecycle.resume) : undefined; if (checkpoint && checkpoint.modelRef !== input.modelRef) throw Error("音频记录与原模型不匹配"); if (!checkpoint) { const item = (await listCanvasAudioModels()).find((x) => x.model.id === input.modelRef); if (!item) throw Error("音频模型未启用,请先在阿里云和模型设置中开通"); const request = buildAudioRequest(input, item.model.modelId); await lifecycle.assertActive(); let response: Response; try { response = await fetch( officialAudioBase(item.provider.baseUrl, item.model.modelId) + request.path, { method: "POST", headers: { Authorization: `Bearer ${item.provider.apiKey}`, "Content-Type": "application/json", }, body: JSON.stringify(request.body), signal: AbortSignal.timeout(input.mode === "music" ? 600000 : 180000), }, ); } catch { throw new ProviderTaskUncertainError( "音频请求连接中断,无法确认是否已受理;请核对服务商记录,系统不会自动重发", ); } let data: any; try { data = await response.json(); } catch { throw new ProviderTaskUncertainError("音频响应无法解析,请核对服务商记录,系统不会自动重发"); } if (!response.ok) throw Error( `阿里云音频请求失败(${response.status},${String(data.code || data.error?.code || "UnknownError").slice(0, 120)})${input.mode === "music" ? ",请确认 Fun-Music 邀测权限已开通" : ""}`, ); const url = data.output?.audio?.url; if (typeof url !== "string" || !/^https?:\/\//i.test(url)) throw new ProviderTaskUncertainError("音频服务未返回有效文件地址,请核对原请求,勿重复生成"); checkpoint = { operation: "audio", url, format: input.format, modelRef: item.model.id, modelId: item.model.modelId, requestId: data.request_id, expiresAt: typeof data.output.audio.expires_at === "number" ? data.output.audio.expires_at : undefined, }; try { await lifecycle.assertActive(); await lifecycle.checkpoint({ upstreamAudio: checkpoint }); } catch { throw new ProviderTaskUncertainError( "音频已生成,但保存任务记录失败;请核对原请求,系统不会自动重发", ); } } if (checkpoint.expiresAt && checkpoint.expiresAt * 1000 < Date.now()) throw Error("阿里云音频临时地址已过期,无法继续转存"); await lifecycle.assertActive(); const dir = await mkdtemp(path.join(os.tmpdir(), "canvas-audio-")); try { const file = path.join(dir, "audio." + checkpoint.format); await downloadCanvasMedia(checkpoint.url, file); const bytes = await readFile(file); if (bytes.length < 12) throw Error("音频内容为空"); const wav = bytes.toString("ascii", 0, 4) === "RIFF" && bytes.toString("ascii", 8, 12) === "WAVE"; const mp3 = bytes.toString("ascii", 0, 3) === "ID3" || (bytes[0] === 0xff && (bytes[1] & 0xe0) === 0xe0); if (!(checkpoint.format === "wav" ? wav : mp3)) throw Error("返回文件不是有效音频格式"); await lifecycle.assertActive(); const saved = await uploadFile( `audio-${crypto.randomUUID()}.${checkpoint.format}`, Uint8Array.from(bytes).buffer, ); return { url: saved.url, modelRef: checkpoint.modelRef, modelId: checkpoint.modelId, metadata: { storageStatus: "archived", format: checkpoint.format, requestId: checkpoint.requestId, }, }; } catch (error) { throw new ProviderTaskUncertainError( `音频已生成,转存暂未完成,将继续转存原文件:${error instanceof Error ? error.message : "存储失败"}`, ); } finally { await rm(dir, { recursive: true, force: true }); } }