/** * GenerateAudio Executor * * Unified executor for generateAudio (TTS) nodes. * Used by both executeWorkflow and regenerateNode. */ import type { GenerateAudioNodeData, SelectedModel } from "@/types"; import { buildGenerateHeaders } from "@/store/utils/buildApiHeaders"; import { runWithFallback } from "./runWithFallback"; import { executePopiartTask } from "./popiartTaskClient"; import type { NodeExecutionContext } from "./types"; import { buildPromptFromConnectedTextItems } from "./textInputPrompt"; import { buildDefaultParametersFromSchema, mergeSchemaDefaults } from "@/utils/modelSchemaDefaults"; import { readAudioGenerationConfig } from "@/utils/generationConfig"; import { createDefaultAudioModel } from "@/store/utils/defaultImageModel"; export interface GenerateAudioOptions { /** When true, falls back to stored inputPrompt if no connections provide it. */ useStoredFallback?: boolean; } export async function executeGenerateAudio( ctx: NodeExecutionContext, _options: GenerateAudioOptions = {} ): Promise { const { node, getConnectedInputs, updateNodeData, getFreshNode, signal, providerSettings, addIncurredCost, } = ctx; void _options; const { text: connectedText, textItems: connectedTextItems = [] } = getConnectedInputs(node.id); // Get fresh node data from store const freshNode = getFreshNode(node.id); const nodeData = (freshNode?.data || node.data) as GenerateAudioNodeData; const nodeConfig = readAudioGenerationConfig(nodeData, createDefaultAudioModel()); const hasConnectedText = connectedTextItems.length > 0 || Boolean(connectedText?.trim()); const text = hasConnectedText ? buildPromptFromConnectedTextItems(connectedTextItems, connectedText, nodeConfig.prompt) : nodeConfig.prompt ?? null; if (!text) { updateNodeData(node.id, { status: "error", error: "Missing text input for audio generation", }); throw new Error("Missing text input for audio generation"); } if (!nodeConfig.selectedModel?.modelId) { updateNodeData(node.id, { status: "error", error: "No model selected", }); throw new Error("No model selected"); } updateNodeData(node.id, { inputPrompt: nodeConfig.prompt, status: "loading", error: null, }); const runOnce = async (modelToUse: SelectedModel, parametersOverride?: Record): Promise => { const provider = modelToUse.provider; const headers = buildGenerateHeaders(provider, providerSettings); const requestPayload = { images: [], prompt: text, selectedModel: modelToUse, parameters: mergeSchemaDefaults( buildDefaultParametersFromSchema(nodeData.parameterSchema ?? []), parametersOverride ?? nodeConfig.parameters ), mediaType: "audio" as const, }; let errorHandled = false; try { const result = await executePopiartTask(requestPayload, { headers, signal }); // Handle audio response (audio or audioUrl field) const audioData = result.audio || result.audioUrl; if (result.success && audioData) { const timestamp = Date.now(); const audioId = `${timestamp}`; // Add to node's audio history const newHistoryItem = { id: audioId, audio: audioData, timestamp, prompt: text || "", model: modelToUse.modelId || "", }; const updatedHistory = [newHistoryItem, ...(nodeData.audioHistory || [])].slice(0, 50); updateNodeData(node.id, { outputAudio: audioData, status: "complete", error: null, audioHistory: updatedHistory, selectedAudioHistoryIndex: 0, }); // Track cost if (modelToUse.provider === "fal" && modelToUse.pricing) { addIncurredCost(modelToUse.pricing.amount); } } else { updateNodeData(node.id, { status: "error", error: result.error || "Audio generation failed", }); errorHandled = true; throw new Error(result.error || "Audio generation failed"); } } catch (error) { if (error instanceof DOMException && error.name === "AbortError") { throw error; } let errorMessage = "Audio generation failed"; if (error instanceof TypeError && error.message.includes("NetworkError")) { errorMessage = "Network error. Check your connection and try again."; } else if (error instanceof TypeError) { errorMessage = `Network error: ${error.message}`; } else if (error instanceof Error) { errorMessage = error.message; } if (!errorHandled) { updateNodeData(node.id, { status: "error", error: errorMessage, }); } throw new Error(errorMessage); } }; await runWithFallback({ nodeId: node.id, primary: nodeConfig.selectedModel, fallback: nodeData.fallbackModel, fallbackParameters: nodeData.fallbackParameters, updateNodeData, runOnce, clearOutput: { outputAudio: null }, }); }