UNPKG

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
"use strict"; 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