This commit is contained in:
Flatlogic Bot 2026-03-02 00:26:20 +00:00
parent 0ccc8a095c
commit 9bcf455037
3 changed files with 178 additions and 389 deletions

View File

@ -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 },

View File

@ -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();

View File

@ -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<any[]>([]);
const [loading, setLoading] = useState(false);
const [isGenerating, setIsGenerating] = useState(false);
const [isGeneratingLyrics, setIsGeneratingLyrics] = useState(false);
const [currentTrack, setCurrentTrack] = useState<any>(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 ? (
<div className="space-y-4">
<div>
<label className="text-[10px] font-black uppercase text-gray-500 mb-2 block">Letras da Música</label>
<div className="flex justify-between items-center mb-2">
<label className="text-[10px] font-black uppercase text-gray-500 block">Letras da Música</label>
<button
onClick={handleGenerateLyrics}
disabled={isGeneratingLyrics}
className="text-[10px] font-black uppercase text-[#00E5FF] flex items-center gap-1 hover:underline disabled:opacity-50"
>
<BaseIcon path={mdiAutoFix} size={14} className={isGeneratingLyrics ? 'animate-spin' : ''} />
Gerar Letras
</button>
</div>
<textarea
value={lyrics}
onChange={(e) => setLyrics(e.target.value)}
@ -442,7 +505,7 @@ const SunoStudio = () => {
<button
onClick={() => handleDownload(currentTrack)}
className="p-2 hover:bg-white/5 rounded-full text-gray-500 hover:text-[#00E5FF] transition-all flex items-center gap-2"
title="Baixar MP3"
title="Baixar MP3/MP4"
>
<BaseIcon path={mdiDownload} size={20} />
<span className="text-[10px] font-black uppercase hidden lg:block">Baixar</span>
@ -558,4 +621,4 @@ SunoStudio.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
};
export default SunoStudio;
export default SunoStudio;