You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

163 lines
5.0 KiB

/**
* 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<void> {
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<string, unknown>): Promise<void> => {
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 },
});
}