UNPKG

@juspay/neurolink

Version:

Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio

334 lines 13.4 kB
/** * Beatoven.ai Music Generation Handler * * Async track-composition API. Submits a compose-track request, polls the * task status, and downloads the resulting audio. * * @module music/providers/BeatovenMusic * @see https://www.beatoven.ai/api-docs */ import { ErrorCategory, ErrorSeverity } from "../../constants/enums.js"; import { logger } from "../../utils/logger.js"; import { MUSIC_ERROR_CODES, MusicError } from "../../utils/musicProcessor.js"; import { MAX_AUDIO_BYTES, readBoundedBuffer } from "../../utils/sizeGuard.js"; import { assertSafeUrl } from "../../utils/ssrfGuard.js"; const DEFAULT_BASE_URL = "https://public-api.beatoven.ai"; const REQUEST_TIMEOUT_MS = 30_000; const POLL_INTERVAL_MS = 3_000; const TOTAL_TIMEOUT_MS = 5 * 60_000; /** * Beatoven.ai Music Generation Handler. * * Beatoven is a royalty-free music generation API tuned for * background / cinematic / brand music. Tracks are composed * asynchronously: submit a prompt, poll the task, then download. */ export class BeatovenMusic { maxDurationSeconds = 300; // 5 minutes per track supportedFormats = [ "mp3", "wav", ]; supportedGenres = [ "ambient", "cinematic", "corporate", "lo-fi", "rock", "pop", "electronic", "orchestral", "folk", ]; apiKey; baseUrl; constructor(apiKey) { const resolved = (apiKey ?? process.env.BEATOVEN_API_KEY ?? "").trim(); this.apiKey = resolved.length > 0 ? resolved : null; this.baseUrl = (process.env.BEATOVEN_BASE_URL ?? DEFAULT_BASE_URL).replace(/\/$/, ""); } isConfigured() { return this.apiKey !== null; } async generate(options) { if (!this.apiKey) { throw new MusicError({ code: MUSIC_ERROR_CODES.PROVIDER_NOT_CONFIGURED, message: "BEATOVEN_API_KEY not configured", category: ErrorCategory.CONFIGURATION, severity: ErrorSeverity.HIGH, retriable: false, }); } const startTime = Date.now(); const requestedFormat = options.format ?? "mp3"; if (!this.supportedFormats.includes(requestedFormat)) { logger.warn(`[BeatovenMusic] Format "${requestedFormat}" not supported — falling back to "mp3"`); } const upstreamFormat = this.supportedFormats.includes(requestedFormat) ? requestedFormat : "mp3"; // 1. Submit compose-track request. const compose = await this.submitCompose(options, upstreamFormat); const taskId = compose.task_id; // 2. Poll until composed or failed. const taskResult = await this.pollUntilComposed(taskId, options.timeout ?? TOTAL_TIMEOUT_MS); const trackUrl = taskResult.meta?.track_url; if (!trackUrl) { throw new MusicError({ code: MUSIC_ERROR_CODES.GENERATION_FAILED, message: `Beatoven task ${taskId} completed but no track_url returned`, category: ErrorCategory.EXECUTION, severity: ErrorSeverity.HIGH, retriable: false, context: { taskId, taskResult }, }); } // 3. Download. const buffer = await this.downloadTrack(trackUrl); const latency = Date.now() - startTime; logger.info(`[BeatovenMusic] Generated ${buffer.length} bytes (${upstreamFormat}) in ${latency}ms — task ${taskId}`); return { buffer, format: upstreamFormat, size: buffer.length, duration: taskResult.meta?.duration, provider: "beatoven", metadata: { latency, provider: "beatoven", model: "beatoven-default", jobId: taskId, trackId: taskResult.meta?.track_id, projectId: taskResult.meta?.project_id, requestedFormat: options.format, }, }; } async submitCompose(options, format) { const durationMs = (options.duration ?? 60) * 1000; const promptParts = [options.prompt]; if (options.genre) { promptParts.push(`Genre: ${options.genre}`); } if (options.mood) { promptParts.push(`Mood: ${options.mood}`); } if (options.tempo !== undefined) { promptParts.push(`Tempo: ${options.tempo} BPM`); } const body = { prompt: { text: promptParts.join(". ") }, duration: durationMs, format, }; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS); let response; try { response = await fetch(`${this.baseUrl}/api/v1/tracks/compose`, { method: "POST", headers: { Authorization: `Bearer ${this.apiKey}`, "Content-Type": "application/json", }, body: JSON.stringify(body), signal: controller.signal, }); } catch (err) { if (err instanceof Error && err.name === "AbortError") { throw new MusicError({ code: MUSIC_ERROR_CODES.GENERATION_FAILED, message: `Beatoven compose request timed out after ${REQUEST_TIMEOUT_MS / 1000}s`, category: ErrorCategory.NETWORK, severity: ErrorSeverity.HIGH, retriable: true, originalError: err, }); } throw new MusicError({ code: MUSIC_ERROR_CODES.GENERATION_FAILED, message: `Beatoven compose network error: ${err instanceof Error ? err.message : String(err)}`, category: ErrorCategory.NETWORK, severity: ErrorSeverity.HIGH, retriable: true, originalError: err instanceof Error ? err : undefined, }); } finally { clearTimeout(timeoutId); } if (!response.ok) { const text = await response.text(); const retriable = response.status === 408 || response.status === 429 || response.status >= 500; throw new MusicError({ code: MUSIC_ERROR_CODES.GENERATION_FAILED, message: `Beatoven compose failed: ${response.status}${text}`, category: retriable ? ErrorCategory.NETWORK : ErrorCategory.EXECUTION, severity: ErrorSeverity.HIGH, retriable, context: { status: response.status, body: text }, }); } const json = (await response.json()); if (!json.task_id) { throw new MusicError({ code: MUSIC_ERROR_CODES.GENERATION_FAILED, message: "Beatoven compose response missing task_id", category: ErrorCategory.EXECUTION, severity: ErrorSeverity.HIGH, retriable: false, context: { response: json }, }); } return json; } async pollUntilComposed(taskId, totalTimeoutMs) { const startTime = Date.now(); while (Date.now() - startTime < totalTimeoutMs) { const status = await this.fetchTaskStatus(taskId); if (status.status === "composed") { return status; } if (status.status === "failed") { throw new MusicError({ code: MUSIC_ERROR_CODES.GENERATION_FAILED, message: `Beatoven task ${taskId} failed: ${status.message ?? "unknown"}`, category: ErrorCategory.EXECUTION, severity: ErrorSeverity.HIGH, retriable: false, context: { taskId, status }, }); } await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS)); } throw new MusicError({ code: MUSIC_ERROR_CODES.POLL_TIMEOUT, message: `Beatoven task ${taskId} did not complete within ${Math.round(totalTimeoutMs / 1000)}s`, category: ErrorCategory.TIMEOUT, severity: ErrorSeverity.MEDIUM, retriable: true, context: { taskId, totalTimeoutMs }, }); } async fetchTaskStatus(taskId) { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS); let response; try { response = await fetch(`${this.baseUrl}/api/v1/tasks/${taskId}`, { method: "GET", headers: { Authorization: `Bearer ${this.apiKey}` }, signal: controller.signal, }); } catch (err) { if (err instanceof Error && err.name === "AbortError") { throw new MusicError({ code: MUSIC_ERROR_CODES.GENERATION_FAILED, message: `Beatoven status poll timed out after ${REQUEST_TIMEOUT_MS / 1000}s`, category: ErrorCategory.NETWORK, severity: ErrorSeverity.MEDIUM, retriable: true, originalError: err, }); } throw new MusicError({ code: MUSIC_ERROR_CODES.GENERATION_FAILED, message: `Beatoven status poll network error: ${err instanceof Error ? err.message : String(err)}`, category: ErrorCategory.NETWORK, severity: ErrorSeverity.MEDIUM, retriable: true, originalError: err instanceof Error ? err : undefined, context: { taskId }, }); } finally { clearTimeout(timeoutId); } if (!response.ok) { const text = await response.text(); throw new MusicError({ code: MUSIC_ERROR_CODES.GENERATION_FAILED, message: `Beatoven status request failed: ${response.status}${text}`, category: ErrorCategory.EXECUTION, severity: ErrorSeverity.MEDIUM, retriable: response.status >= 500, context: { status: response.status, taskId }, }); } return (await response.json()); } async downloadTrack(url) { try { await assertSafeUrl(url); } catch (err) { throw new MusicError({ code: MUSIC_ERROR_CODES.GENERATION_FAILED, message: `Beatoven track URL rejected: ${err instanceof Error ? err.message : String(err)}`, category: ErrorCategory.VALIDATION, severity: ErrorSeverity.HIGH, retriable: false, context: { url }, }); } const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS); let response; try { response = await fetch(url, { signal: controller.signal }); } catch (err) { if (err instanceof Error && err.name === "AbortError") { throw new MusicError({ code: MUSIC_ERROR_CODES.GENERATION_FAILED, message: `Beatoven track download timed out after ${REQUEST_TIMEOUT_MS / 1000}s`, category: ErrorCategory.NETWORK, severity: ErrorSeverity.MEDIUM, retriable: true, originalError: err, }); } throw new MusicError({ code: MUSIC_ERROR_CODES.GENERATION_FAILED, message: `Beatoven track download network error: ${err instanceof Error ? err.message : String(err)}`, category: ErrorCategory.NETWORK, severity: ErrorSeverity.MEDIUM, retriable: true, originalError: err instanceof Error ? err : undefined, context: { url }, }); } finally { clearTimeout(timeoutId); } if (!response.ok) { throw new MusicError({ code: MUSIC_ERROR_CODES.GENERATION_FAILED, message: `Beatoven track download failed: ${response.status}`, category: ErrorCategory.NETWORK, severity: ErrorSeverity.MEDIUM, retriable: response.status >= 500, context: { status: response.status, url }, }); } try { return await readBoundedBuffer(response, MAX_AUDIO_BYTES, "Beatoven track"); } catch (err) { throw new MusicError({ code: MUSIC_ERROR_CODES.GENERATION_FAILED, message: `Beatoven track download exceeded size limit: ${err instanceof Error ? err.message : String(err)}`, category: ErrorCategory.EXECUTION, severity: ErrorSeverity.HIGH, retriable: false, context: { url }, }); } } } //# sourceMappingURL=BeatovenMusic.js.map