n8n-nodes-aimlapi
Version:
Custom n8n node for integrating with the AI/ML API platform (AIMLAPI) to interact with LLMs and multimodal AI models such as chat completion endpoints.
389 lines • 15.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.executeSpeechTranscription = executeSpeechTranscription;
const crypto_1 = require("crypto");
const promises_1 = require("timers/promises");
const request_1 = require("../utils/request");
const WAITING_STATUSES = new Set(['waiting', 'active', 'processing', 'queued', 'pending']);
const FAILURE_STATUSES = new Set(['failed', 'error', 'cancelled', 'canceled', 'timeout']);
const POLL_INTERVAL_MS = 5000;
const MAX_POLL_ATTEMPTS = 120;
class MultipartFormBuilder {
constructor() {
this.parts = [];
}
append(name, value, options = {}) {
this.parts.push({ name, value, ...options });
}
build() {
const boundary = `----n8n-aimlapi-${(0, crypto_1.randomBytes)(16).toString('hex')}`;
const chunks = [];
for (const part of this.parts) {
const headerLines = [];
const safeName = part.name.replace(/"/g, '\\"');
let disposition = `form-data; name="${safeName}"`;
if (part.filename) {
const safeFilename = part.filename.replace(/"/g, '\\"');
disposition += `; filename="${safeFilename}"`;
}
headerLines.push(`Content-Disposition: ${disposition}`);
if (part.contentType) {
headerLines.push(`Content-Type: ${part.contentType}`);
}
chunks.push(Buffer.from(`--${boundary}\r\n${headerLines.join('\r\n')}\r\n\r\n`, 'utf8'));
chunks.push(typeof part.value === 'string' ? Buffer.from(part.value, 'utf8') : part.value);
chunks.push(Buffer.from('\r\n', 'utf8'));
}
chunks.push(Buffer.from(`--${boundary}--\r\n`, 'utf8'));
const body = Buffer.concat(chunks);
return {
body,
headers: {
'Content-Type': `multipart/form-data; boundary=${boundary}`,
'Content-Length': body.length.toString(),
},
};
}
}
function toList(value) {
if (typeof value !== 'string') {
return [];
}
return value
.split(',')
.map((entry) => entry.trim())
.filter((entry) => entry.length > 0);
}
function applyOptionsToJson(target, options) {
const listOptions = [
['custom_intent', 'customIntent'],
['custom_topic', 'customTopic'],
['tag', 'tags'],
];
for (const [jsonKey, optionKey] of listOptions) {
const values = toList(options[optionKey]);
if (values.length === 1) {
target[jsonKey] = values[0];
}
else if (values.length > 1) {
target[jsonKey] = values;
}
}
const stringOptions = [
['language', 'language'],
['keywords', 'keywords'],
['summarize', 'summarize'],
['search', 'search'],
['extra', 'extra'],
['diarize_version', 'diarizeVersion'],
];
for (const [jsonKey, optionKey] of stringOptions) {
const value = options[optionKey];
if (typeof value === 'string' && value.trim() !== '') {
target[jsonKey] = value;
}
}
const numericOptions = [['utt_split', 'uttSplit']];
for (const [jsonKey, optionKey] of numericOptions) {
const value = options[optionKey];
if (typeof value === 'number' && !Number.isNaN(value)) {
target[jsonKey] = value;
}
}
const booleanOptions = [
['detect_language', 'detectLanguage'],
['detect_entities', 'detectEntities'],
['detect_topics', 'detectTopics'],
['diarize', 'diarize'],
['dictation', 'dictation'],
['filler_words', 'fillerWords'],
['intents', 'intents'],
['measurements', 'measurements'],
['multi_channel', 'multiChannel'],
['numerals', 'numerals'],
['paragraphs', 'paragraphs'],
['profanity_filter', 'profanityFilter'],
['punctuate', 'punctuate'],
['sentiment', 'sentiment'],
['smart_format', 'smartFormat'],
['topics', 'topics'],
['utterances', 'utterances'],
];
for (const [jsonKey, optionKey] of booleanOptions) {
const value = options[optionKey];
if (typeof value === 'boolean') {
target[jsonKey] = value;
}
}
const enumOptions = [
['custom_intent_mode', 'customIntentMode'],
['custom_topic_mode', 'customTopicMode'],
];
for (const [jsonKey, optionKey] of enumOptions) {
const value = options[optionKey];
if (typeof value === 'string' && value.trim() !== '') {
target[jsonKey] = value;
}
}
}
function applyOptionsToForm(form, options) {
const appendList = (key, sourceKey) => {
const values = toList(options[sourceKey]);
if (values.length === 0) {
return;
}
if (values.length === 1) {
form.append(key, values[0]);
return;
}
for (const value of values) {
form.append(key, value);
}
};
appendList('custom_intent', 'customIntent');
appendList('custom_topic', 'customTopic');
appendList('tag', 'tags');
const appendString = (key, sourceKey) => {
const value = options[sourceKey];
if (typeof value === 'string' && value.trim() !== '') {
form.append(key, value);
}
};
appendString('language', 'language');
appendString('keywords', 'keywords');
appendString('summarize', 'summarize');
appendString('search', 'search');
appendString('extra', 'extra');
appendString('diarize_version', 'diarizeVersion');
const appendBoolean = (key, sourceKey) => {
const value = options[sourceKey];
if (typeof value === 'boolean') {
form.append(key, value ? 'true' : 'false');
}
};
appendBoolean('detect_language', 'detectLanguage');
appendBoolean('detect_entities', 'detectEntities');
appendBoolean('detect_topics', 'detectTopics');
appendBoolean('diarize', 'diarize');
appendBoolean('dictation', 'dictation');
appendBoolean('filler_words', 'fillerWords');
appendBoolean('intents', 'intents');
appendBoolean('measurements', 'measurements');
appendBoolean('multi_channel', 'multiChannel');
appendBoolean('numerals', 'numerals');
appendBoolean('paragraphs', 'paragraphs');
appendBoolean('profanity_filter', 'profanityFilter');
appendBoolean('punctuate', 'punctuate');
appendBoolean('sentiment', 'sentiment');
appendBoolean('smart_format', 'smartFormat');
appendBoolean('topics', 'topics');
appendBoolean('utterances', 'utterances');
const appendEnum = (key, sourceKey) => {
const value = options[sourceKey];
if (typeof value === 'string' && value.trim() !== '') {
form.append(key, value);
}
};
appendEnum('custom_intent_mode', 'customIntentMode');
appendEnum('custom_topic_mode', 'customTopicMode');
const value = options.uttSplit;
if (typeof value === 'number' && !Number.isNaN(value)) {
form.append('utt_split', value.toString());
}
}
function extractGenerationId(response) {
if (typeof response.generation_id === 'string') {
return response.generation_id;
}
if (response.result && typeof response.result === 'object') {
const nested = response.result;
if (typeof nested.generation_id === 'string') {
return nested.generation_id;
}
}
return undefined;
}
function shouldContinuePolling(status) {
if (typeof status !== 'string') {
return true;
}
const normalized = status.trim().toLowerCase();
if (normalized.length === 0) {
return true;
}
if (WAITING_STATUSES.has(normalized)) {
return true;
}
return false;
}
function extractErrorMessage(response) {
const candidates = [response.message, response.error];
for (const candidate of candidates) {
if (typeof candidate === 'string' && candidate.trim() !== '') {
return candidate;
}
}
if (response.result && typeof response.result === 'object') {
const nested = response.result;
const nestedCandidates = [nested.message, nested.error];
for (const candidate of nestedCandidates) {
if (typeof candidate === 'string' && candidate.trim() !== '') {
return candidate;
}
}
}
return undefined;
}
function getResultPayload(response) {
if (response.result && typeof response.result === 'object') {
return response.result;
}
return response;
}
function pickTranscriptFromAlternatives(result) {
const results = result.results;
if (!results) {
return undefined;
}
const channels = results.channels ?? [];
for (const channel of channels) {
if (!channel || typeof channel !== 'object') {
continue;
}
const alternatives = channel.alternatives ?? [];
for (const alternative of alternatives) {
if (!alternative || typeof alternative !== 'object') {
continue;
}
const transcript = alternative.transcript;
if (typeof transcript === 'string' && transcript.trim() !== '') {
return transcript;
}
}
}
return undefined;
}
function extractTranscriptText(payload) {
const directCandidates = [payload.text, payload.transcript, payload.output];
for (const candidate of directCandidates) {
if (typeof candidate === 'string' && candidate.trim() !== '') {
return candidate;
}
}
const fromAlternatives = pickTranscriptFromAlternatives(payload);
if (fromAlternatives) {
return fromAlternatives;
}
return '';
}
function extractSegments(payload) {
if (Array.isArray(payload.segments)) {
return payload.segments;
}
const results = payload.results;
if (!results) {
return [];
}
const collected = [];
const channels = results.channels ?? [];
for (const channel of channels) {
if (!channel || typeof channel !== 'object') {
continue;
}
const alternatives = channel.alternatives ?? [];
for (const alternative of alternatives) {
if (!alternative || typeof alternative !== 'object') {
continue;
}
const segments = alternative.segments ?? [];
if (Array.isArray(segments) && segments.length > 0) {
collected.push(...segments.filter((segment) => typeof segment === 'object'));
continue;
}
const words = alternative.words ?? [];
if (Array.isArray(words) && words.length > 0) {
collected.push(...words.filter((word) => typeof word === 'object'));
}
}
}
return collected;
}
async function pollForTranscription(context, baseURL, generationId) {
for (let attempt = 0; attempt < MAX_POLL_ATTEMPTS; attempt++) {
if (attempt > 0) {
await (0, promises_1.setTimeout)(POLL_INTERVAL_MS);
}
const requestOptions = (0, request_1.createRequestOptions)(baseURL, `/v1/stt/${encodeURIComponent(generationId)}`, 'GET');
requestOptions.body = undefined;
const response = (await context.helpers.httpRequestWithAuthentication.call(context, 'aimlApi', requestOptions));
const status = response.status;
const normalizedStatus = typeof status === 'string' && status.trim() !== '' ? status.trim().toLowerCase() : undefined;
if (normalizedStatus && FAILURE_STATUSES.has(normalizedStatus)) {
const reason = extractErrorMessage(response);
const suffix = reason ? `: ${reason}` : '';
throw new Error(`Transcription failed with status "${normalizedStatus}"${suffix}`);
}
if (shouldContinuePolling(status)) {
continue;
}
return response;
}
throw new Error(`Timed out while waiting for transcription (generation_id: ${generationId})`);
}
async function executeSpeechTranscription({ context, itemIndex, baseURL, model, }) {
const inputSource = context.getNodeParameter('transcriptionInputSource', itemIndex);
const extract = context.getNodeParameter('transcriptionExtract', itemIndex);
const options = context.getNodeParameter('transcriptionOptions', itemIndex, {});
let creationResponse;
if (inputSource === 'url') {
const audioUrl = context.getNodeParameter('transcriptionAudioUrl', itemIndex);
if (!audioUrl || audioUrl.trim() === '') {
throw new Error('Audio URL is required when using the remote URL input source.');
}
const body = {
model,
url: audioUrl,
};
applyOptionsToJson(body, options);
const requestOptions = (0, request_1.createRequestOptions)(baseURL, '/v1/stt/create');
requestOptions.body = body;
creationResponse = (await context.helpers.httpRequestWithAuthentication.call(context, 'aimlApi', requestOptions));
}
else {
const binaryPropertyName = context.getNodeParameter('transcriptionBinaryProperty', itemIndex);
const binaryData = await context.helpers.getBinaryDataBuffer(itemIndex, binaryPropertyName);
const binaryMetadata = context.helpers.assertBinaryData(itemIndex, binaryPropertyName);
const form = new MultipartFormBuilder();
form.append('model', model);
const filename = binaryMetadata.fileName ?? `audio.${binaryMetadata.fileExtension ?? 'wav'}`;
const contentType = binaryMetadata.mimeType ?? 'application/octet-stream';
form.append('audio', binaryData, {
filename,
contentType,
});
applyOptionsToForm(form, options);
const { body: formBody, headers } = form.build();
const requestOptions = (0, request_1.createRequestOptions)(baseURL, '/v1/stt/create', 'POST', {
json: false,
headers,
});
requestOptions.body = formBody;
creationResponse = (await context.helpers.httpRequestWithAuthentication.call(context, 'aimlApi', requestOptions));
}
const generationId = extractGenerationId(creationResponse);
if (!generationId) {
throw new Error('No generation_id returned from the speech transcription request.');
}
const finalResponse = await pollForTranscription(context, baseURL, generationId);
if (extract === 'raw') {
return { json: { result: finalResponse }, pairedItem: { item: itemIndex } };
}
const resultPayload = getResultPayload(finalResponse);
if (extract === 'segments') {
const segments = extractSegments(resultPayload);
return { json: { segments }, pairedItem: { item: itemIndex } };
}
const text = extractTranscriptText(resultPayload);
return { json: { text }, pairedItem: { item: itemIndex } };
}
//# sourceMappingURL=speechTranscription.execute.js.map