@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
JavaScript
/**
* 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