diff --git a/README.md b/README.md index edb4163..ae62c25 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,8 @@ Suno API currently mainly implements the following APIs: If no IDs are provided, all music will be returned. - `/api/get_limit`: Get quota Info - `/api/extend_audio`: Extend audio length +- `/api/generate_stems`: Make stem tracks (separate audio and music track) +- `/api/get_aligned_lyrics`: Get list of timestamps for each word in the lyrics - `/api/clip`: Get clip information based on ID passed as query parameter `id` - `/api/concat`: Generate the whole song from extensions ``` diff --git a/README_CN.md b/README_CN.md index 5c15789..eee70fe 100644 --- a/README_CN.md +++ b/README_CN.md @@ -123,6 +123,8 @@ Suno API 目前主要实现了以下 API: - `/api/get`: 根据id获取音乐信息。获取多个请用","分隔,不传ids则返回所有音乐 - `/api/get_limit`: 获取配额信息 - `/api/extend_audio`: 在一首音乐的基础上,扩展音乐长度 +- `/api/generate_stems`: 制作主干轨道(单独的音频和音乐轨道 +- `/api/get_aligned_lyrics`: 获取歌词中每个单词的时间戳列表 - `/api/clip`: 检索特定音乐的信息 - `/api/concat`: 合并音乐,将扩展后的音乐和原始音乐合并 ``` diff --git a/src/app/api/generate_stems/route.ts b/src/app/api/generate_stems/route.ts new file mode 100644 index 0000000..7070d90 --- /dev/null +++ b/src/app/api/generate_stems/route.ts @@ -0,0 +1,69 @@ +import { NextResponse, NextRequest } from "next/server"; +import { DEFAULT_MODEL, sunoApi } from "@/lib/SunoApi"; +import { corsHeaders } from "@/lib/utils"; + +export const dynamic = "force-dynamic"; + +export async function POST(req: NextRequest) { + if (req.method === 'POST') { + try { + const body = await req.json(); + const { audio_id } = body; + + if (!audio_id) { + return new NextResponse(JSON.stringify({ error: 'Audio ID is required' }), { + status: 400, + headers: { + 'Content-Type': 'application/json', + ...corsHeaders + } + }); + } + + const audioInfo = await (await sunoApi) + .generateStems(audio_id); + + return new NextResponse(JSON.stringify(audioInfo), { + status: 200, + headers: { + 'Content-Type': 'application/json', + ...corsHeaders + } + }); + } catch (error: any) { + console.error('Error generating stems:', JSON.stringify(error.response.data)); + if (error.response.status === 402) { + return new NextResponse(JSON.stringify({ error: error.response.data.detail }), { + status: 402, + headers: { + 'Content-Type': 'application/json', + ...corsHeaders + } + }); + } + return new NextResponse(JSON.stringify({ error: 'Internal server error: ' + JSON.stringify(error.response.data.detail) }), { + status: 500, + headers: { + 'Content-Type': 'application/json', + ...corsHeaders + } + }); + } + } else { + return new NextResponse('Method Not Allowed', { + headers: { + Allow: 'POST', + ...corsHeaders + }, + status: 405 + }); + } +} + + +export async function OPTIONS(request: Request) { + return new Response(null, { + status: 200, + headers: corsHeaders + }); +} \ No newline at end of file diff --git a/src/app/api/get_aligned_lyrics/route.ts b/src/app/api/get_aligned_lyrics/route.ts new file mode 100644 index 0000000..11681e4 --- /dev/null +++ b/src/app/api/get_aligned_lyrics/route.ts @@ -0,0 +1,60 @@ +import { NextResponse, NextRequest } from "next/server"; +import { sunoApi } from "@/lib/SunoApi"; +import { corsHeaders } from "@/lib/utils"; + +export const dynamic = "force-dynamic"; + +export async function GET(req: NextRequest) { + if (req.method === 'GET') { + try { + const url = new URL(req.url); + const song_id = url.searchParams.get('song_id'); + + if (!song_id) { + return new NextResponse(JSON.stringify({ error: 'Song ID is required' }), { + status: 400, + headers: { + 'Content-Type': 'application/json', + ...corsHeaders + } + }); + } + + const lyricAlignment = await (await sunoApi).getLyricAlignment(song_id); + + + return new NextResponse(JSON.stringify(lyricAlignment), { + status: 200, + headers: { + 'Content-Type': 'application/json', + ...corsHeaders + } + }); + } catch (error) { + console.error('Error fetching lyric alignment:', error); + + return new NextResponse(JSON.stringify({ error: 'Internal server error. ' + error }), { + status: 500, + headers: { + 'Content-Type': 'application/json', + ...corsHeaders + } + }); + } + } else { + return new NextResponse('Method Not Allowed', { + headers: { + Allow: 'GET', + ...corsHeaders + }, + status: 405 + }); + } +} + +export async function OPTIONS(request: Request) { + return new Response(null, { + status: 200, + headers: corsHeaders + }); +} \ No newline at end of file diff --git a/src/app/docs/page.tsx b/src/app/docs/page.tsx index a053eed..379201c 100644 --- a/src/app/docs/page.tsx +++ b/src/app/docs/page.tsx @@ -29,6 +29,8 @@ export default function Docs() { ids. If no IDs are provided, all music will be returned. - \`/api/get_limit\`: Get quota Info - \`/api/extend_audio\`: Extend audio length +- \`/api/generate_stems\`: Make stem tracks (separate audio and music track) +- \`/api/get_aligned_lyrics\`: Get list of timestamps for each word in the lyrics - \`/api/clip\`: Get clip information based on ID passed as query parameter \`id\` - \`/api/concat\`: Generate the whole song from extensions \`\`\` diff --git a/src/app/docs/swagger-suno-api.json b/src/app/docs/swagger-suno-api.json index d00b676..5aa47e8 100644 --- a/src/app/docs/swagger-suno-api.json +++ b/src/app/docs/swagger-suno-api.json @@ -243,6 +243,35 @@ } } }, + "/api/generate_stems": { + "post": { + "summary": "Make stem tracks (separate audio and music track).", + "description": "Make stem tracks (separate audio and music track).", + "tags": ["default"], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["audio_id"], + "properties": { + "audio_id": { + "type": "string", + "description": "The ID of the song to generate stems for.", + "example": "e76498dc-6ab4-4a10-a19f-8a095790e28d" + } + } + } + } + } + }, + "responses": { + "200": { + "$ref": "#/components/schemas/audio_info" + } + } + } + }, "/api/generate_lyrics": { "post": { "summary": "Generate lyrics based on Prompt.", @@ -387,6 +416,29 @@ } } }, + "/api/get_aligned_lyrics": { + "get": { + "summary": "Get lyric alignment.", + "description": "Get lyric alignment.", + "tags": ["default"], + "parameters": [ + { + "name": "song_id", + "in": "query", + "required": true, + "description": "Song ID", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "$ref": "#/components/schemas/audio_info" + } + } + } + }, "/api/clip": { "get": { "summary": "Get clip information based on ID.", diff --git a/src/app/page.tsx b/src/app/page.tsx index b945dff..89f7f4a 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -106,6 +106,8 @@ Suno API currently mainly implements the following APIs: - \`/api/get?ids=\`: Get music Info by id, separate multiple id with ",". - \`/api/get_limit\`: Get quota Info - \`/api/extend_audio\`: Extend audio length +- \`/api/generate_stems\`: Make stem tracks (separate audio and music track) +- \`/api/get_aligned_lyrics\`: Get list of timestamps for each word in the lyrics - \`/api/concat\`: Generate the whole song from extensions \`\`\` diff --git a/src/lib/SunoApi.ts b/src/lib/SunoApi.ts index 62d1b76..5eead26 100644 --- a/src/lib/SunoApi.ts +++ b/src/lib/SunoApi.ts @@ -385,6 +385,48 @@ class SunoApi { return response.data; } + /** + * Generate stems for a song. + * @param song_id The ID of the song to generate stems for. + * @returns A promise that resolves to an AudioInfo object representing the generated stems. + */ + public async generateStems(song_id: string): Promise { + await this.keepAlive(false); + const response = await this.client.post( + `${SunoApi.BASE_URL}/api/edit/stems/${song_id}`, {} + ); + + console.log('generateStems response:\n', response?.data); + return response.data.clips.map((clip: any) => ({ + id: clip.id, + status: clip.status, + created_at: clip.created_at, + title: clip.title, + stem_from_id: clip.metadata.stem_from_id, + duration: clip.metadata.duration + })); + } + + + /** + * Get the lyric alignment for a song. + * @param song_id The ID of the song to get the lyric alignment for. + * @returns A promise that resolves to an object containing the lyric alignment. + */ + public async getLyricAlignment(song_id: string): Promise { + await this.keepAlive(false); + const response = await this.client.get(`${SunoApi.BASE_URL}/api/gen/${song_id}/aligned_lyrics/v2/`); + + console.log(`getLyricAlignment ~ response:`, response.data); + return response.data?.aligned_words.map((transcribedWord: any) => ({ + word: transcribedWord.word, + start_s: transcribedWord.start_s, + end_s: transcribedWord.end_s, + success: transcribedWord.success, + p_align: transcribedWord.p_align + })); + } + /** * Processes the lyrics (prompt) from the audio metadata into a more readable format. * @param prompt The original lyrics text.