diff --git a/.env.example b/.env.example index c2d29888120..5d2efc7e2f0 100644 --- a/.env.example +++ b/.env.example @@ -777,3 +777,6 @@ EMAIL_INCOMING_HOST=imap.example.com EMAIL_INCOMING_PORT=993 # Default port for secure IMAP EMAIL_INCOMING_USER= EMAIL_INCOMING_PASS= + +# Suno AI Music Generation +SUNO_API_KEY= diff --git a/agent/package.json b/agent/package.json index 8dc1061ef25..852b8f49618 100644 --- a/agent/package.json +++ b/agent/package.json @@ -116,6 +116,7 @@ "@elizaos/plugin-0x": "workspace:*", "@elizaos/plugin-dkg": "workspace:*", "@elizaos/plugin-email": "workspace:*", + "@elizaos/plugin-suno": "workspace:*", "readline": "1.3.0", "ws": "8.18.0", "yargs": "17.7.2" diff --git a/agent/src/index.ts b/agent/src/index.ts index 72e0208f450..0373edbf3d0 100644 --- a/agent/src/index.ts +++ b/agent/src/index.ts @@ -127,6 +127,7 @@ import path from "path"; import { fileURLToPath } from "url"; import yargs from "yargs"; import { emailPlugin } from "@elizaos/plugin-email"; +import { sunoPlugin } from "@elizaos/plugin-suno"; const __filename = fileURLToPath(import.meta.url); // get the resolved path to the file const __dirname = path.dirname(__filename); // get the name of the directory @@ -1127,7 +1128,8 @@ export async function createAgent( : null, getSecret(character, "EMAIL_INCOMING_USER") && getSecret(character, "EMAIL_INCOMING_PASS") || getSecret(character, "EMAIL_OUTGOING_USER") && getSecret(character, "EMAIL_OUTGOING_PASS") ? - emailPlugin : null + emailPlugin : null, + getSecret(character, "SUNO_API_KEY") ? sunoPlugin : null ].filter(Boolean), providers: [], actions: [], diff --git a/packages/plugin-suno/README.md b/packages/plugin-suno/README.md new file mode 100644 index 00000000000..fd09d802764 --- /dev/null +++ b/packages/plugin-suno/README.md @@ -0,0 +1,134 @@ +@elizaos/plugin-suno + +A Suno AI music generation plugin for ElizaOS that enables AI-powered music creation and audio manipulation. + +OVERVIEW + +The Suno plugin integrates Suno AI's powerful music generation capabilities into ElizaOS, providing a seamless way to: +- Generate music from text prompts +- Create custom music with fine-tuned parameters +- Extend existing audio tracks + +Original Plugin: https://github.com/gcui-art/suno-api?tab=readme-ov-file + +INSTALLATION + + npm install @elizaos/plugin-suno + +QUICK START + +1. Register the plugin with ElizaOS: + + import { sunoPlugin } from '@elizaos/plugin-suno'; + import { Eliza } from '@elizaos/eliza'; + + const eliza = new Eliza(); + eliza.registerPlugin(sunoPlugin); + +2. Configure the Suno provider with your API credentials: + + import { sunoProvider } from '@elizaos/plugin-suno'; + + sunoProvider.configure({ + apiKey: 'your-suno-api-key' + }); + +FEATURES + +1. Generate Music + Generate music using predefined settings and a text prompt: + + await eliza.execute('suno.generate', { + prompt: "An upbeat electronic dance track with energetic beats" + }); + +2. Custom Music Generation + Create music with detailed control over generation parameters: + + await eliza.execute('suno.customGenerate', { + prompt: "A melodic piano piece with soft strings", + temperature: 0.8, + duration: 30 + }); + +3. Extend Audio + Extend existing audio tracks to create longer compositions: + + await eliza.execute('suno.extend', { + audioFile: "path/to/audio.mp3", + duration: 60 + }); + +API REFERENCE + +SunoProvider Configuration +The Suno provider accepts the following configuration options: + + interface SunoConfig { + apiKey: string; + } + +Action Parameters: + +1. Generate Music + interface GenerateMusicParams { + prompt: string; + } + +2. Custom Generate Music + interface CustomGenerateMusicParams { + prompt: string; + temperature?: number; + duration?: number; + } + +3. Extend Audio + interface ExtendAudioParams { + audioFile: string; + duration: number; + } + +ERROR HANDLING + +The plugin includes built-in error handling for common scenarios: + + try { + await eliza.execute('suno.generate', params); + } catch (error) { + if (error.code === 'SUNO_API_ERROR') { + // Handle API-specific errors + } + // Handle other errors + } + +EXAMPLES + +Creating a Complete Song: + + const result = await eliza.execute('suno.generate', { + prompt: "Create a pop song with vocals, drums, and guitar", + duration: 180 // 3 minutes + }); + + console.log(`Generated music file: ${result.outputPath}`); + +Extending an Existing Track: + + const extended = await eliza.execute('suno.extend', { + audioFile: "original-track.mp3", + duration: 60 // Add 60 seconds + }); + + console.log(`Extended audio file: ${extended.outputPath}`); + + +LICENSE + +MIT + +SUPPORT + +For issues and feature requests, please open an issue on our GitHub repository. + +--- +Built with ❤️ for ElizaOS diff --git a/packages/plugin-suno/eslint.config.mjs b/packages/plugin-suno/eslint.config.mjs new file mode 100644 index 00000000000..92fe5bbebef --- /dev/null +++ b/packages/plugin-suno/eslint.config.mjs @@ -0,0 +1,3 @@ +import eslintGlobalConfig from "../../eslint.config.mjs"; + +export default [...eslintGlobalConfig]; diff --git a/packages/plugin-suno/package.json b/packages/plugin-suno/package.json new file mode 100644 index 00000000000..caaa9ca57d7 --- /dev/null +++ b/packages/plugin-suno/package.json @@ -0,0 +1,23 @@ +{ + "name": "@elizaos/plugin-suno", + "version": "0.1.9-alpha.1", + "description": "Suno AI Music Generation Plugin for Eliza", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc", + "test": "jest" + }, + "dependencies": { + "@elizaos/core": "^0.1.0" + }, + "devDependencies": { + "typescript": "^4.9.0", + "@types/node": "^16.0.0", + "jest": "^27.0.0", + "@types/jest": "^27.0.0" + }, + "peerDependencies": { + "@elizaos/eliza": "^0.1.0" + } +} \ No newline at end of file diff --git a/packages/plugin-suno/src/actions/customGenerate.ts b/packages/plugin-suno/src/actions/customGenerate.ts new file mode 100644 index 00000000000..8762c2a2aaa --- /dev/null +++ b/packages/plugin-suno/src/actions/customGenerate.ts @@ -0,0 +1,32 @@ +import { type Action } from "@elizaos/core"; +import { SunoProvider } from "../providers/suno"; +import { CustomGenerateParams, GenerationResponse } from "../types"; + +const customGenerateMusic: Action = { + name: "custom-generate-music", + description: "Generate music with custom parameters using Suno AI", + provider: "suno", + + async execute(params: CustomGenerateParams, provider: SunoProvider): Promise { + const response = await provider.request('/custom-generate', { + method: 'POST', + body: JSON.stringify({ + prompt: params.prompt, + duration: params.duration || 30, + temperature: params.temperature || 1.0, + top_k: params.topK || 250, + top_p: params.topP || 0.95, + classifier_free_guidance: params.classifier_free_guidance || 3.0, + reference_audio: params.reference_audio, + style: params.style, + bpm: params.bpm, + key: params.key, + mode: params.mode + }) + }); + + return response; + } +}; + +export default customGenerateMusic; \ No newline at end of file diff --git a/packages/plugin-suno/src/actions/extend.ts b/packages/plugin-suno/src/actions/extend.ts new file mode 100644 index 00000000000..193ab5db7a4 --- /dev/null +++ b/packages/plugin-suno/src/actions/extend.ts @@ -0,0 +1,23 @@ +import { Action } from "@elizaos/eliza"; +import { SunoProvider } from "../providers/suno"; +import { ExtendParams, GenerationResponse } from "../types"; + +const extendAudio: Action = { + name: "extend-audio", + description: "Extend the duration of an existing audio generation", + provider: "suno", + + async execute(params: ExtendParams, provider: SunoProvider): Promise { + const response = await provider.request('/extend', { + method: 'POST', + body: JSON.stringify({ + audio_id: params.audio_id, + duration: params.duration + }) + }); + + return response; + } +}; + +export default extendAudio; \ No newline at end of file diff --git a/packages/plugin-suno/src/actions/generate.ts b/packages/plugin-suno/src/actions/generate.ts new file mode 100644 index 00000000000..dd9a50946f3 --- /dev/null +++ b/packages/plugin-suno/src/actions/generate.ts @@ -0,0 +1,27 @@ +import { Action } from "@elizaos/eliza"; +import { SunoProvider } from "../providers/suno"; +import { GenerateParams, GenerationResponse } from "../types"; + +const generateMusic: Action = { + name: "generate-music", + description: "Generate music using Suno AI", + provider: "suno", + + async execute(params: GenerateParams, provider: SunoProvider): Promise { + const response = await provider.request('/generate', { + method: 'POST', + body: JSON.stringify({ + prompt: params.prompt, + duration: params.duration || 30, + temperature: params.temperature || 1.0, + top_k: params.topK || 250, + top_p: params.topP || 0.95, + classifier_free_guidance: params.classifier_free_guidance || 3.0 + }) + }); + + return response; + } +}; + +export default generateMusic; \ No newline at end of file diff --git a/packages/plugin-suno/src/index.ts b/packages/plugin-suno/src/index.ts new file mode 100644 index 00000000000..08e02103ea5 --- /dev/null +++ b/packages/plugin-suno/src/index.ts @@ -0,0 +1,22 @@ +import { Plugin } from "@elizaos/eliza"; +import generateMusic from "./actions/generate"; +import customGenerateMusic from "./actions/customGenerate"; +import extendAudio from "./actions/extend"; +import { SunoProvider, sunoProvider } from "./providers/suno"; + +export { + SunoProvider, + generateMusic as GenerateMusic, + customGenerateMusic as CustomGenerateMusic, + extendAudio as ExtendAudio +}; + +export const sunoPlugin: Plugin = { + name: "suno", + description: "Suno AI Music Generation Plugin for Eliza", + actions: [generateMusic, customGenerateMusic, extendAudio], + evaluators: [], + providers: [sunoProvider], +}; + +export default sunoPlugin; \ No newline at end of file diff --git a/packages/plugin-suno/src/providers/suno.ts b/packages/plugin-suno/src/providers/suno.ts new file mode 100644 index 00000000000..a32b212a114 --- /dev/null +++ b/packages/plugin-suno/src/providers/suno.ts @@ -0,0 +1,67 @@ +import { Provider } from "@elizaos/eliza"; + +export interface SunoConfig { + apiKey: string; + baseUrl?: string; +} + +export class SunoProvider implements Provider { + private apiKey: string; + private baseUrl: string; + + constructor(config: SunoConfig) { + this.apiKey = config.apiKey; + this.baseUrl = config.baseUrl || 'https://api.suno.ai/v1'; + } + + async request(endpoint: string, options: RequestInit = {}) { + const url = `${this.baseUrl}${endpoint}`; + const headers = { + 'Authorization': `Bearer ${this.apiKey}`, + 'Content-Type': 'application/json', + ...options.headers, + }; + + const response = await fetch(url, { + ...options, + headers, + }); + + if (!response.ok) { + throw new Error(`Suno API error: ${response.statusText}`); + } + + return response.json(); + } +} + +export const sunoProvider = new SunoProvider({ apiKey: process.env.SUNO_API_KEY || '' }); + +export interface GenerateParams { + prompt: string; + duration?: number; + temperature?: number; + topK?: number; + topP?: number; + classifier_free_guidance?: number; +} + +export interface CustomGenerateParams extends GenerateParams { + reference_audio?: string; + style?: string; + bpm?: number; + key?: string; + mode?: string; +} + +export interface ExtendParams { + audio_id: string; + duration: number; +} + +export interface GenerationResponse { + id: string; + status: 'pending' | 'processing' | 'completed' | 'failed'; + audio_url?: string; + error?: string; +} \ No newline at end of file diff --git a/packages/plugin-suno/src/types/index.ts b/packages/plugin-suno/src/types/index.ts new file mode 100644 index 00000000000..8ea38ff2fef --- /dev/null +++ b/packages/plugin-suno/src/types/index.ts @@ -0,0 +1,34 @@ +export interface GenerateParams { + prompt: string; + duration?: number; + temperature?: number; + topK?: number; + topP?: number; + classifier_free_guidance?: number; +} + +export interface CustomGenerateParams { + prompt: string; + duration?: number; + temperature?: number; + topK?: number; + topP?: number; + classifier_free_guidance?: number; + reference_audio?: string; + style?: string; + bpm?: number; + key?: string; + mode?: string; +} + +export interface ExtendParams { + audio_id: string; + duration: number; +} + +export interface GenerationResponse { + id: string; + status: string; + url?: string; + error?: string; +} \ No newline at end of file diff --git a/packages/plugin-suno/tsconfig.json b/packages/plugin-suno/tsconfig.json new file mode 100644 index 00000000000..73993deaaf7 --- /dev/null +++ b/packages/plugin-suno/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../core/tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": [ + "src/**/*.ts" + ] +} \ No newline at end of file diff --git a/packages/plugin-suno/tsup.config.ts b/packages/plugin-suno/tsup.config.ts new file mode 100644 index 00000000000..dd25475bb63 --- /dev/null +++ b/packages/plugin-suno/tsup.config.ts @@ -0,0 +1,29 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + outDir: "dist", + sourcemap: true, + clean: true, + format: ["esm"], // Ensure you're targeting CommonJS + external: [ + "dotenv", // Externalize dotenv to prevent bundling + "fs", // Externalize fs to use Node.js built-in module + "path", // Externalize other built-ins if necessary + "@reflink/reflink", + "@node-llama-cpp", + "https", + "http", + "agentkeepalive", + "safe-buffer", + "base-x", + "bs58", + "borsh", + "@solana/buffer-layout", + "stream", + "buffer", + "querystring", + "amqplib", + // Add other modules you want to externalize + ], +});