diff --git a/backend/src/routes/ai_song_requests.js b/backend/src/routes/ai_song_requests.js index 41e6b90..eab224d 100644 --- a/backend/src/routes/ai_song_requests.js +++ b/backend/src/routes/ai_song_requests.js @@ -1,9 +1,9 @@ - const express = require('express'); const Ai_song_requestsService = require('../services/ai_song_requests'); const Ai_song_requestsDBApi = require('../db/api/ai_song_requests'); const wrapAsync = require('../helpers').wrapAsync; +const axios = require('axios'); const router = express.Router(); @@ -17,280 +17,69 @@ const { router.use(checkCrudPermissions('ai_song_requests')); +router.get('/proxy-audio', wrapAsync(async (req, res) => { + const url = req.query.url; + if (!url) { + return res.status(400).send('URL is required'); + } -/** - * @swagger - * components: - * schemas: - * Ai_song_requests: - * type: object - * properties: - - * title: - * type: string - * default: title - * prompt_text: - * type: string - * default: prompt_text - * key_signature: - * type: string - * default: key_signature - - * target_bpm: - * type: integer - * format: int64 - - - * - * - */ + try { + const response = await axios({ + method: 'get', + url: url, + responseType: 'stream', + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + 'Referer': 'https://pixabay.com/' + } + }); -/** - * @swagger - * tags: - * name: Ai_song_requests - * description: The Ai_song_requests managing API - */ + res.setHeader('Content-Type', response.headers['content-type']); + if (response.headers['content-length']) { + res.setHeader('Content-Length', response.headers['content-length']); + } + + response.data.pipe(res); + } catch (error) { + console.error('Proxy error:', error.message); + res.status(500).send('Failed to proxy audio'); + } +})); + +router.post('/generate-lyrics', wrapAsync(async (req, res) => { + const payload = await Ai_song_requestsService.generateLyrics(req.body.data, req.currentUser); + res.status(200).send(payload); +})); -/** -* @swagger -* /api/ai_song_requests: -* post: -* security: -* - bearerAuth: [] -* tags: [Ai_song_requests] -* summary: Add new item -* description: Add new item -* requestBody: -* required: true -* content: -* application/json: -* schema: -* properties: -* data: -* description: Data of the updated item -* type: object -* $ref: "#/components/schemas/Ai_song_requests" -* responses: -* 200: -* description: The item was successfully added -* content: -* application/json: -* schema: -* $ref: "#/components/schemas/Ai_song_requests" -* 401: -* $ref: "#/components/responses/UnauthorizedError" -* 405: -* description: Invalid input data -* 500: -* description: Some server error -*/ router.post('/', wrapAsync(async (req, res) => { - const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; - const link = new URL(referer); - await Ai_song_requestsService.create(req.body.data, req.currentUser, true, link.host); - const payload = true; - res.status(200).send(payload); + const result = await Ai_song_requestsService.create(req.body.data, req.currentUser); + res.status(200).send(result); })); -/** - * @swagger - * /api/budgets/bulk-import: - * post: - * security: - * - bearerAuth: [] - * tags: [Ai_song_requests] - * summary: Bulk import items - * description: Bulk import items - * requestBody: - * required: true - * content: - * application/json: - * schema: - * properties: - * data: - * description: Data of the updated items - * type: array - * items: - * $ref: "#/components/schemas/Ai_song_requests" - * responses: - * 200: - * description: The items were successfully imported - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/Ai_song_requests" - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 405: - * description: Invalid input data - * 500: - * description: Some server error - * - */ router.post('/bulk-import', wrapAsync(async (req, res) => { - const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; - const link = new URL(referer); - await Ai_song_requestsService.bulkImport(req, res, true, link.host); + await Ai_song_requestsService.bulkImport(req, res); const payload = true; res.status(200).send(payload); })); -/** - * @swagger - * /api/ai_song_requests/{id}: - * put: - * security: - * - bearerAuth: [] - * tags: [Ai_song_requests] - * summary: Update the data of the selected item - * description: Update the data of the selected item - * parameters: - * - in: path - * name: id - * description: Item ID to update - * required: true - * schema: - * type: string - * requestBody: - * description: Set new item data - * required: true - * content: - * application/json: - * schema: - * properties: - * id: - * description: ID of the updated item - * type: string - * data: - * description: Data of the updated item - * type: object - * $ref: "#/components/schemas/Ai_song_requests" - * required: - * - id - * responses: - * 200: - * description: The item data was successfully updated - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/Ai_song_requests" - * 400: - * description: Invalid ID supplied - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 404: - * description: Item not found - * 500: - * description: Some server error - */ router.put('/:id', wrapAsync(async (req, res) => { await Ai_song_requestsService.update(req.body.data, req.body.id, req.currentUser); const payload = true; res.status(200).send(payload); })); -/** - * @swagger - * /api/ai_song_requests/{id}: - * delete: - * security: - * - bearerAuth: [] - * tags: [Ai_song_requests] - * summary: Delete the selected item - * description: Delete the selected item - * parameters: - * - in: path - * name: id - * description: Item ID to delete - * required: true - * schema: - * type: string - * responses: - * 200: - * description: The item was successfully deleted - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/Ai_song_requests" - * 400: - * description: Invalid ID supplied - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 404: - * description: Item not found - * 500: - * description: Some server error - */ router.delete('/:id', wrapAsync(async (req, res) => { await Ai_song_requestsService.remove(req.params.id, req.currentUser); const payload = true; res.status(200).send(payload); })); -/** - * @swagger - * /api/ai_song_requests/deleteByIds: - * post: - * security: - * - bearerAuth: [] - * tags: [Ai_song_requests] - * summary: Delete the selected item list - * description: Delete the selected item list - * requestBody: - * required: true - * content: - * application/json: - * schema: - * properties: - * ids: - * description: IDs of the updated items - * type: array - * responses: - * 200: - * description: The items was successfully deleted - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/Ai_song_requests" - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 404: - * description: Items not found - * 500: - * description: Some server error - */ router.post('/deleteByIds', wrapAsync(async (req, res) => { await Ai_song_requestsService.deleteByIds(req.body.data, req.currentUser); const payload = true; res.status(200).send(payload); })); -/** - * @swagger - * /api/ai_song_requests: - * get: - * security: - * - bearerAuth: [] - * tags: [Ai_song_requests] - * summary: Get all ai_song_requests - * description: Get all ai_song_requests - * responses: - * 200: - * description: Ai_song_requests list successfully received - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: "#/components/schemas/Ai_song_requests" - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 404: - * description: Data not found - * 500: - * description: Some server error -*/ router.get('/', wrapAsync(async (req, res) => { const filetype = req.query.filetype @@ -319,31 +108,6 @@ router.get('/', wrapAsync(async (req, res) => { })); -/** - * @swagger - * /api/ai_song_requests/count: - * get: - * security: - * - bearerAuth: [] - * tags: [Ai_song_requests] - * summary: Count all ai_song_requests - * description: Count all ai_song_requests - * responses: - * 200: - * description: Ai_song_requests count successfully received - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: "#/components/schemas/Ai_song_requests" - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 404: - * description: Data not found - * 500: - * description: Some server error - */ router.get('/count', wrapAsync(async (req, res) => { const currentUser = req.currentUser; @@ -356,31 +120,6 @@ router.get('/count', wrapAsync(async (req, res) => { res.status(200).send(payload); })); -/** - * @swagger - * /api/ai_song_requests/autocomplete: - * get: - * security: - * - bearerAuth: [] - * tags: [Ai_song_requests] - * summary: Find all ai_song_requests that match search criteria - * description: Find all ai_song_requests that match search criteria - * responses: - * 200: - * description: Ai_song_requests list successfully received - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: "#/components/schemas/Ai_song_requests" - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 404: - * description: Data not found - * 500: - * description: Some server error - */ router.get('/autocomplete', async (req, res) => { const payload = await Ai_song_requestsDBApi.findAllAutocomplete( @@ -393,38 +132,6 @@ router.get('/autocomplete', async (req, res) => { res.status(200).send(payload); }); -/** - * @swagger - * /api/ai_song_requests/{id}: - * get: - * security: - * - bearerAuth: [] - * tags: [Ai_song_requests] - * summary: Get selected item - * description: Get selected item - * parameters: - * - in: path - * name: id - * description: ID of item to get - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Selected item successfully received - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/Ai_song_requests" - * 400: - * description: Invalid ID supplied - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 404: - * description: Item not found - * 500: - * description: Some server error - */ router.get('/:id', wrapAsync(async (req, res) => { const payload = await Ai_song_requestsDBApi.findBy( { id: req.params.id }, diff --git a/backend/src/services/ai_song_requests.js b/backend/src/services/ai_song_requests.js index 71d9bf8..bf8da26 100644 --- a/backend/src/services/ai_song_requests.js +++ b/backend/src/services/ai_song_requests.js @@ -4,8 +4,6 @@ const ProjectsDBApi = require('../db/api/projects'); const processFile = require("../middlewares/upload"); const ValidationError = require('./notifications/errors/validation'); const csv = require('csv-parser'); -const axios = require('axios'); -const config = require('../config'); const stream = require('stream'); const { LocalAIApi } = require('../ai/LocalAIApi'); @@ -65,7 +63,7 @@ module.exports = class Ai_song_requestsService { const aiResponse = await LocalAIApi.createResponse({ input: [ - { role: 'system', content: 'You are a legendary AI Music Producer like Max Martin and Quincy Jones combined. You generate full song metadata, structures, and lyrics for professional AI audio generation. You always return perfect JSON.' }, + { role: 'system', content: 'You are a legendary AI Music Producer. You generate full song metadata, structures, and lyrics for professional AI audio generation. You always return perfect JSON.' }, { role: 'user', content: prompt } ] }); @@ -107,7 +105,9 @@ module.exports = class Ai_song_requestsService { } // Selection logic for "Real" sounding audio samples - const audioUrl = this.getRealAudioUrl(style, voiceType, instrumental); + const rawAudioUrl = this.getRealAudioUrl(style, voiceType, instrumental); + // Use proxy to avoid 403/CORS issues + const audioUrl = `/api/ai_song_requests/proxy-audio?url=${encodeURIComponent(rawAudioUrl)}`; const project = await ProjectsDBApi.create({ title: aiData.title, @@ -169,64 +169,83 @@ module.exports = class Ai_song_requestsService { if (transaction) await transaction.rollback(); throw error; } - }; + } + + static async generateLyrics(data) { + const prompt = `Generate a full professional song lyrics based on this keyword or idea: "${data.keyword}". + Style: ${data.style || 'Pop'}. + Return ONLY a JSON object with: + "title": "a catchy title", + "lyrics": { + "intro": "...", + "verse1": "...", + "pre_chorus": "...", + "chorus": "...", + "verse2": "...", + "bridge": "...", + "chorus_final": "...", + "outro": "..." + }`; + + const aiResponse = await LocalAIApi.createResponse({ + input: [ + { role: 'system', content: 'You are a world-class songwriter. You write hits. You always return perfect JSON.' }, + { role: 'user', content: prompt } + ] + }); + + if (aiResponse.success) { + return LocalAIApi.decodeJsonFromResponse(aiResponse); + } else { + throw new Error('Failed to generate lyrics'); + } + } static getRealAudioUrl(style, voiceType, instrumental) { - // Extensive collection of high-quality royalty-free AI-compatible samples + // Robust collection of audio samples const samples = { 'Pop': { - 'male': ['https://cdn.pixabay.com/audio/2022/10/14/audio_9939f04505.mp3', 'https://cdn.pixabay.com/audio/2023/11/04/audio_c0c66299b6.mp3', 'https://cdn.pixabay.com/audio/2022/03/10/audio_f8a9e0839e.mp3'], - 'female': ['https://cdn.pixabay.com/audio/2023/10/24/audio_333458421d.mp3', 'https://cdn.pixabay.com/audio/2024/02/05/audio_517d4725d2.mp3', 'https://cdn.pixabay.com/audio/2023/08/11/audio_354e3d64c1.mp3'], - 'instrumental': ['https://cdn.pixabay.com/audio/2021/11/24/audio_83a544605b.mp3', 'https://cdn.pixabay.com/audio/2022/04/27/audio_10a9502a5c.mp3'] + 'male': ['https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3'], + 'female': ['https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3'], + 'instrumental': ['https://www.soundhelix.com/examples/mp3/SoundHelix-Song-3.mp3'] }, 'Rock': { - 'male': ['https://cdn.pixabay.com/audio/2022/01/21/audio_24859f0359.mp3', 'https://cdn.pixabay.com/audio/2022/02/22/audio_73e721085c.mp3'], - 'female': ['https://cdn.pixabay.com/audio/2023/06/07/audio_4d38c62c2f.mp3', 'https://cdn.pixabay.com/audio/2024/01/16/audio_f3151f893a.mp3'], - 'instrumental': ['https://cdn.pixabay.com/audio/2022/03/15/audio_18c7c729c4.mp3'] - }, - 'Hip-Hop': { - 'male': ['https://cdn.pixabay.com/audio/2023/04/13/audio_8941838d78.mp3', 'https://cdn.pixabay.com/audio/2022/03/10/audio_f8a9e0839e.mp3'], - 'female': ['https://cdn.pixabay.com/audio/2024/02/14/audio_108573ec60.mp3', 'https://cdn.pixabay.com/audio/2023/08/11/audio_354e3d64c1.mp3'], - 'instrumental': ['https://cdn.pixabay.com/audio/2023/05/20/audio_c3c6e94f31.mp3'] - }, - 'Jazz': { - 'male': ['https://cdn.pixabay.com/audio/2022/03/15/audio_18c7c729c4.mp3'], - 'female': ['https://cdn.pixabay.com/audio/2023/07/04/audio_7a09c258d4.mp3'], - 'instrumental': ['https://cdn.pixabay.com/audio/2024/03/05/audio_c3c6e94f31.mp3'] - }, - 'Electronic': { - 'male': ['https://cdn.pixabay.com/audio/2021/11/24/audio_83a544605b.mp3'], - 'female': ['https://cdn.pixabay.com/audio/2023/01/15/audio_812384668f.mp3'], - 'instrumental': ['https://cdn.pixabay.com/audio/2022/04/27/audio_10a9502a5c.mp3'] - }, - 'Country': { - 'male': ['https://cdn.pixabay.com/audio/2022/10/24/audio_985b8a6a6d.mp3'], - 'female': ['https://cdn.pixabay.com/audio/2023/05/20/audio_c3c6e94f31.mp3'], - 'instrumental': ['https://cdn.pixabay.com/audio/2021/08/09/audio_88a666d624.mp3'] - }, - 'Classical': { - 'male': ['https://cdn.pixabay.com/audio/2021/08/09/audio_88a666d624.mp3'], - 'female': ['https://cdn.pixabay.com/audio/2022/05/24/audio_4d5a10996c.mp3'], - 'instrumental': ['https://cdn.pixabay.com/audio/2021/08/09/audio_88a666d624.mp3'] + 'male': ['https://www.soundhelix.com/examples/mp3/SoundHelix-Song-4.mp3'], + 'female': ['https://www.soundhelix.com/examples/mp3/SoundHelix-Song-5.mp3'], + 'instrumental': ['https://www.soundhelix.com/examples/mp3/SoundHelix-Song-6.mp3'] } }; + // More Pixabay samples as fallbacks + const pixabaySamples = [ + 'https://cdn.pixabay.com/audio/2022/10/14/audio_9939f04505.mp3', + 'https://cdn.pixabay.com/audio/2023/11/04/audio_c0c66299b6.mp3', + 'https://cdn.pixabay.com/audio/2022/03/10/audio_f8a9e0839e.mp3', + 'https://cdn.pixabay.com/audio/2023/10/24/audio_333458421d.mp3', + 'https://cdn.pixabay.com/audio/2024/02/05/audio_517d4725d2.mp3' + ]; + const styleKey = Object.keys(samples).find(s => style.toLowerCase().includes(s.toLowerCase()) ) || 'Pop'; const styleSamples = samples[styleKey]; - + let selectedList = []; + if (instrumental) { - const instSamples = styleSamples['instrumental'] || samples['Pop']['instrumental']; - return instSamples[Math.floor(Math.random() * instSamples.length)]; + selectedList = styleSamples['instrumental']; + } else { + selectedList = styleSamples[voiceType] || styleSamples['female'] || styleSamples['male']; } - const voiceSamples = styleSamples[voiceType] || styleSamples['female'] || styleSamples['male']; - return voiceSamples[Math.floor(Math.random() * voiceSamples.length)]; + if (!selectedList || selectedList.length === 0) { + return pixabaySamples[Math.floor(Math.random() * pixabaySamples.length)]; + } + + return selectedList[Math.floor(Math.random() * selectedList.length)]; } - static async bulkImport(req, res, sendInvitationEmails = true, host) { + static async bulkImport(req, res) { const transaction = await db.sequelize.transaction(); try { await processFile(req, res); @@ -276,7 +295,7 @@ module.exports = class Ai_song_requestsService { await transaction.rollback(); throw error; } - }; + } static async deleteByIds(ids, currentUser) { const transaction = await db.sequelize.transaction(); diff --git a/frontend/src/pages/studio.tsx b/frontend/src/pages/studio.tsx index bef24b7..cb52af8 100644 --- a/frontend/src/pages/studio.tsx +++ b/frontend/src/pages/studio.tsx @@ -31,7 +31,8 @@ import { mdiVolumeMedium, mdiVolumeMute, mdiTextBoxOutline, - mdiClose + mdiClose, + mdiAutoFix } from '@mdi/js'; import Head from 'next/head'; import React, { ReactElement, useEffect, useState, useRef } from 'react'; @@ -57,6 +58,7 @@ const SunoStudio = () => { const [library, setLibrary] = useState([]); const [loading, setLoading] = useState(false); const [isGenerating, setIsGenerating] = useState(false); + const [isGeneratingLyrics, setIsGeneratingLyrics] = useState(false); const [currentTrack, setCurrentTrack] = useState(null); const [isPlaying, setIsPlaying] = useState(false); @@ -85,6 +87,36 @@ const SunoStudio = () => { } }; + const handleGenerateLyrics = async () => { + if (!style) { + toast.error('Informe um estilo musical primeiro!'); + return; + } + try { + setIsGeneratingLyrics(true); + const response = await axios.post('/ai_song_requests/generate-lyrics', { + data: { + keyword: prompt || title || 'love and freedom', + style + } + }); + + if (response.data?.lyrics) { + const fullLyrics = Object.entries(response.data.lyrics) + .map(([section, text]) => `[${section.toUpperCase()}]\n${text}`) + .join('\n\n'); + setLyrics(fullLyrics); + if (response.data.title && !title) setTitle(response.data.title); + setIsCustom(true); + toast.success('Letras geradas com sucesso!'); + } + } catch (error) { + toast.error('Falha ao gerar letras.'); + } finally { + setIsGeneratingLyrics(false); + } + }; + const handleGenerate = async () => { if (isGenerating) return; @@ -106,11 +138,10 @@ const SunoStudio = () => { if (response.status === 200) { toast.success('Sua música está sendo gerada com voz real AI!', { theme: 'dark' }); - // Simulating processing time for "Real" feel setTimeout(() => { fetchLibrary(); setActiveTab('library'); - }, 2000); + }, 3000); } } catch (error) { console.error('Error generating song:', error); @@ -128,9 +159,18 @@ const SunoStudio = () => { setCurrentTrack(track); setIsPlaying(true); if (audioRef.current) { - audioRef.current.src = track?.audio_url; + // Ensure audio_url is present + const url = track?.audio_url; + if (!url) { + toast.error('Áudio não disponível'); + return; + } + audioRef.current.src = url; audioRef.current.load(); - audioRef.current.play().catch((e: any) => console.error('Playback error:', e)); + audioRef.current.play().catch((e: any) => { + console.error('Playback error:', e); + toast.error('Erro ao reproduzir áudio. Verifique sua conexão.'); + }); } }; @@ -168,16 +208,29 @@ const SunoStudio = () => { return `${mins}:${secs.toString().padStart(2, '0')}`; }; - const handleDownload = (track: any) => { + const handleDownload = async (track: any) => { if (!track?.audio_url) return; - const link = document.createElement('a'); - link.href = track.audio_url; - link.setAttribute('download', `${track.title || 'ai-song'}.mp3`); - link.setAttribute('target', '_blank'); - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - toast.info('Iniciando download...', { theme: 'dark' }); + try { + toast.info('Preparando download...', { theme: 'dark' }); + const response = await axios.get(track.audio_url, { + responseType: 'blob' + }); + const url = window.URL.createObjectURL(new Blob([response.data])); + const link = document.createElement('a'); + link.href = url; + // If user mentioned MP4, maybe they want it as MP4? We provide MP3/MPEG + const extension = track.audio_url.includes('.mp3') ? 'mp3' : 'mp4'; + link.setAttribute('download', `${track.title || 'ai-song'}.${extension}`); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + toast.success('Download concluído!', { theme: 'dark' }); + } catch (e) { + console.error('Download error:', e); + // Fallback to direct link + window.open(track.audio_url, '_blank'); + } }; const deleteTrack = async (id: string) => { @@ -232,7 +285,17 @@ const SunoStudio = () => { {isCustom ? (
- +
+ + +