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
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 },
|
|
});
|
|
}
|
|
|