12
This commit is contained in:
parent
2c36af7989
commit
c1ff6b0871
@ -14,11 +14,13 @@ module.exports = class Ai_song_requestsService {
|
|||||||
const isCustom = data.is_custom === true || data.is_custom === 'true';
|
const isCustom = data.is_custom === true || data.is_custom === 'true';
|
||||||
const voiceType = data.voice_type || 'female';
|
const voiceType = data.voice_type || 'female';
|
||||||
const style = data.style || 'Pop';
|
const style = data.style || 'Pop';
|
||||||
|
const language = data.language || 'English';
|
||||||
const instrumental = data.instrumental === true || data.instrumental === 'true';
|
const instrumental = data.instrumental === true || data.instrumental === 'true';
|
||||||
|
|
||||||
let prompt = '';
|
let prompt = '';
|
||||||
if (isCustom) {
|
if (isCustom) {
|
||||||
prompt = `Based on these lyrics: "${data.lyrics}", and style: "${style}". ${instrumental ? 'This should be a purely instrumental track with NO vocals.' : `Use a ${voiceType} artificial AI voice.`}
|
prompt = `Based on these lyrics: "${data.lyrics}", and style: "${style}". Language: ${language}.
|
||||||
|
${instrumental ? 'This should be a purely instrumental track with NO vocals.' : `Use a ${voiceType} artificial AI voice synchronized with the text.`}
|
||||||
Create a full professional studio song structure.
|
Create a full professional studio song structure.
|
||||||
Return ONLY a JSON object with:
|
Return ONLY a JSON object with:
|
||||||
"title": "${data.title || 'a creative song title'}",
|
"title": "${data.title || 'a creative song title'}",
|
||||||
@ -27,7 +29,7 @@ module.exports = class Ai_song_requestsService {
|
|||||||
"mood": "detailed emotional description",
|
"mood": "detailed emotional description",
|
||||||
"instruments": ["detailed list of instruments used"],
|
"instruments": ["detailed list of instruments used"],
|
||||||
"arrangement": "step-by-step description of the song flow",
|
"arrangement": "step-by-step description of the song flow",
|
||||||
"tags": ["style", "genre", "vibe"],
|
"tags": ["${style}", "${language}", "vibe"],
|
||||||
"lyrics": {
|
"lyrics": {
|
||||||
"intro": "[Musical Intro description]",
|
"intro": "[Musical Intro description]",
|
||||||
"verse1": "...",
|
"verse1": "...",
|
||||||
@ -37,10 +39,17 @@ module.exports = class Ai_song_requestsService {
|
|||||||
"bridge": "...",
|
"bridge": "...",
|
||||||
"chorus_final": "...",
|
"chorus_final": "...",
|
||||||
"outro": "[Outro description]"
|
"outro": "[Outro description]"
|
||||||
}`;
|
},
|
||||||
|
"synchronized_lyrics": [
|
||||||
|
{"time": 0, "text": "[Intro]", "type": "intro"},
|
||||||
|
{"time": 10, "text": "First line of verse...", "type": "verse1"},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
(The synchronized_lyrics should cover the whole song duration (~120-180s) with realistic timings for each line)`;
|
||||||
} else {
|
} else {
|
||||||
prompt = `Create a complete song configuration based on this idea: "${data.prompt_text}". Style: ${style}. ${instrumental ? 'This should be an instrumental track.' : `The song should feature a ${voiceType} lead AI vocal.`}
|
prompt = `Create a complete song configuration based on this idea: "${data.prompt_text}". Style: ${style}. Language: ${language}.
|
||||||
Generate high-quality lyrics including Intro, Verses, Chorus, Bridge, and Outro.
|
${instrumental ? 'This should be an instrumental track.' : `The song should feature a ${voiceType} lead AI vocal synchronized with the generated lyrics.`}
|
||||||
|
Generate high-quality lyrics in ${language} including Intro, Verses, Chorus, Bridge, and Outro.
|
||||||
Return ONLY a JSON object with:
|
Return ONLY a JSON object with:
|
||||||
"title": "a catchy creative song title",
|
"title": "a catchy creative song title",
|
||||||
"bpm": a number between 70-150,
|
"bpm": a number between 70-150,
|
||||||
@ -48,7 +57,7 @@ module.exports = class Ai_song_requestsService {
|
|||||||
"mood": "vibrant emotional description",
|
"mood": "vibrant emotional description",
|
||||||
"instruments": ["list of realistic instruments"],
|
"instruments": ["list of realistic instruments"],
|
||||||
"arrangement": "professional song structure",
|
"arrangement": "professional song structure",
|
||||||
"tags": ["tag1", "tag2", "tag3"],
|
"tags": ["${style}", "${language}", "tag3"],
|
||||||
"lyrics": {
|
"lyrics": {
|
||||||
"intro": "[Musical atmosphere]",
|
"intro": "[Musical atmosphere]",
|
||||||
"verse1": "detailed verse lyrics...",
|
"verse1": "detailed verse lyrics...",
|
||||||
@ -58,12 +67,18 @@ module.exports = class Ai_song_requestsService {
|
|||||||
"bridge": "emotional bridge...",
|
"bridge": "emotional bridge...",
|
||||||
"chorus_final": "final grand chorus...",
|
"chorus_final": "final grand chorus...",
|
||||||
"outro": "fading outro..."
|
"outro": "fading outro..."
|
||||||
}`;
|
},
|
||||||
|
"synchronized_lyrics": [
|
||||||
|
{"time": 0, "text": "[Intro]", "type": "intro"},
|
||||||
|
{"time": 10, "text": "First generated line...", "type": "verse1"},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
(Provide realistic timestamps for a 3-minute song)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const aiResponse = await LocalAIApi.createResponse({
|
const aiResponse = await LocalAIApi.createResponse({
|
||||||
input: [
|
input: [
|
||||||
{ 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: 'system', content: 'You are a legendary AI Music Producer supporting 200+ languages. You generate full song metadata, structures, and lyrics for professional AI audio generation with vocal synchronization. You always return perfect JSON.' },
|
||||||
{ role: 'user', content: prompt }
|
{ role: 'user', content: prompt }
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
@ -74,12 +89,13 @@ module.exports = class Ai_song_requestsService {
|
|||||||
key: 'G Major',
|
key: 'G Major',
|
||||||
mood: 'Energetic',
|
mood: 'Energetic',
|
||||||
instruments: ['Drums', 'Bass', 'Synthesizer', 'Electric Guitar'],
|
instruments: ['Drums', 'Bass', 'Synthesizer', 'Electric Guitar'],
|
||||||
tags: [style, 'Studio AI', 'Professional'],
|
tags: [style, language, 'Studio AI', 'Professional'],
|
||||||
lyrics: {
|
lyrics: {
|
||||||
intro: '[Fade in]',
|
intro: '[Fade in]',
|
||||||
verse1: 'Verse content goes here...',
|
verse1: 'Verse content goes here...',
|
||||||
chorus: 'Main chorus content...'
|
chorus: 'Main chorus content...'
|
||||||
}
|
},
|
||||||
|
synchronized_lyrics: []
|
||||||
};
|
};
|
||||||
|
|
||||||
if (aiResponse.success) {
|
if (aiResponse.success) {
|
||||||
@ -92,6 +108,7 @@ module.exports = class Ai_song_requestsService {
|
|||||||
original_request: {
|
original_request: {
|
||||||
style,
|
style,
|
||||||
voiceType,
|
voiceType,
|
||||||
|
language,
|
||||||
isCustom,
|
isCustom,
|
||||||
instrumental,
|
instrumental,
|
||||||
prompt_text: data.prompt_text,
|
prompt_text: data.prompt_text,
|
||||||
@ -106,7 +123,6 @@ module.exports = class Ai_song_requestsService {
|
|||||||
|
|
||||||
// Selection logic for "Real" sounding audio samples
|
// Selection logic for "Real" sounding audio samples
|
||||||
const rawAudioUrl = this.getRealAudioUrl(style, voiceType, instrumental);
|
const rawAudioUrl = this.getRealAudioUrl(style, voiceType, instrumental);
|
||||||
// Use proxy to avoid 403/CORS issues - Store WITHOUT /api prefix for axios compatibility
|
|
||||||
const audioUrl = `/ai_song_requests/proxy-audio?url=${encodeURIComponent(rawAudioUrl)}`;
|
const audioUrl = `/ai_song_requests/proxy-audio?url=${encodeURIComponent(rawAudioUrl)}`;
|
||||||
|
|
||||||
const project = await ProjectsDBApi.create({
|
const project = await ProjectsDBApi.create({
|
||||||
@ -171,7 +187,7 @@ module.exports = class Ai_song_requestsService {
|
|||||||
|
|
||||||
static async generateLyrics(data) {
|
static async generateLyrics(data) {
|
||||||
const prompt = `Generate a full professional song lyrics based on this keyword or idea: "${data.keyword}".
|
const prompt = `Generate a full professional song lyrics based on this keyword or idea: "${data.keyword}".
|
||||||
Style: ${data.style || 'Pop'}.
|
Style: ${data.style || 'Pop'}. Language: ${data.language || 'Portuguese'}.
|
||||||
Return ONLY a JSON object with:
|
Return ONLY a JSON object with:
|
||||||
"title": "a catchy title",
|
"title": "a catchy title",
|
||||||
"lyrics": {
|
"lyrics": {
|
||||||
@ -187,7 +203,7 @@ module.exports = class Ai_song_requestsService {
|
|||||||
|
|
||||||
const aiResponse = await LocalAIApi.createResponse({
|
const aiResponse = await LocalAIApi.createResponse({
|
||||||
input: [
|
input: [
|
||||||
{ role: 'system', content: 'You are a world-class songwriter. You write hits. You always return perfect JSON.' },
|
{ role: 'system', content: 'You are a world-class songwriter supporting 200+ languages. You write hits. You always return perfect JSON.' },
|
||||||
{ role: 'user', content: prompt }
|
{ role: 'user', content: prompt }
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
@ -200,7 +216,7 @@ module.exports = class Ai_song_requestsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static getRealAudioUrl(style, voiceType, instrumental) {
|
static getRealAudioUrl(style, voiceType, instrumental) {
|
||||||
// Robust collection of audio samples
|
// Robust collection of audio samples with expanded styles (Brazilian, American, etc.)
|
||||||
const samples = {
|
const samples = {
|
||||||
'Pop': {
|
'Pop': {
|
||||||
'male': ['https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3'],
|
'male': ['https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3'],
|
||||||
@ -211,10 +227,49 @@ module.exports = class Ai_song_requestsService {
|
|||||||
'male': ['https://www.soundhelix.com/examples/mp3/SoundHelix-Song-4.mp3'],
|
'male': ['https://www.soundhelix.com/examples/mp3/SoundHelix-Song-4.mp3'],
|
||||||
'female': ['https://www.soundhelix.com/examples/mp3/SoundHelix-Song-5.mp3'],
|
'female': ['https://www.soundhelix.com/examples/mp3/SoundHelix-Song-5.mp3'],
|
||||||
'instrumental': ['https://www.soundhelix.com/examples/mp3/SoundHelix-Song-6.mp3']
|
'instrumental': ['https://www.soundhelix.com/examples/mp3/SoundHelix-Song-6.mp3']
|
||||||
|
},
|
||||||
|
'Jazz': {
|
||||||
|
'male': ['https://www.soundhelix.com/examples/mp3/SoundHelix-Song-7.mp3'],
|
||||||
|
'female': ['https://www.soundhelix.com/examples/mp3/SoundHelix-Song-8.mp3'],
|
||||||
|
'instrumental': ['https://www.soundhelix.com/examples/mp3/SoundHelix-Song-9.mp3']
|
||||||
|
},
|
||||||
|
'Electronic': {
|
||||||
|
'male': ['https://www.soundhelix.com/examples/mp3/SoundHelix-Song-10.mp3'],
|
||||||
|
'female': ['https://www.soundhelix.com/examples/mp3/SoundHelix-Song-11.mp3'],
|
||||||
|
'instrumental': ['https://www.soundhelix.com/examples/mp3/SoundHelix-Song-12.mp3']
|
||||||
|
},
|
||||||
|
'Hip Hop': {
|
||||||
|
'male': ['https://www.soundhelix.com/examples/mp3/SoundHelix-Song-13.mp3'],
|
||||||
|
'female': ['https://www.soundhelix.com/examples/mp3/SoundHelix-Song-14.mp3'],
|
||||||
|
'instrumental': ['https://www.soundhelix.com/examples/mp3/SoundHelix-Song-15.mp3']
|
||||||
|
},
|
||||||
|
'Country': {
|
||||||
|
'male': ['https://www.soundhelix.com/examples/mp3/SoundHelix-Song-16.mp3'],
|
||||||
|
'female': ['https://cdn.pixabay.com/audio/2022/03/10/audio_f8a9e0839e.mp3'],
|
||||||
|
'instrumental': ['https://cdn.pixabay.com/audio/2023/11/04/audio_c0c66299b6.mp3']
|
||||||
|
},
|
||||||
|
'Samba': {
|
||||||
|
'male': ['https://cdn.pixabay.com/audio/2022/01/21/audio_31cc5963c1.mp3'],
|
||||||
|
'female': ['https://cdn.pixabay.com/audio/2022/01/21/audio_31cc5963c1.mp3'],
|
||||||
|
'instrumental': ['https://cdn.pixabay.com/audio/2022/01/21/audio_31cc5963c1.mp3']
|
||||||
|
},
|
||||||
|
'Bossa Nova': {
|
||||||
|
'male': ['https://cdn.pixabay.com/audio/2023/06/11/audio_658e658e65.mp3'],
|
||||||
|
'female': ['https://cdn.pixabay.com/audio/2023/06/11/audio_658e658e65.mp3'],
|
||||||
|
'instrumental': ['https://cdn.pixabay.com/audio/2023/06/11/audio_658e658e65.mp3']
|
||||||
|
},
|
||||||
|
'Funk': {
|
||||||
|
'male': ['https://cdn.pixabay.com/audio/2022/08/04/audio_658e658e65.mp3'],
|
||||||
|
'female': ['https://cdn.pixabay.com/audio/2022/08/04/audio_658e658e65.mp3'],
|
||||||
|
'instrumental': ['https://cdn.pixabay.com/audio/2022/08/04/audio_658e658e65.mp3']
|
||||||
|
},
|
||||||
|
'Sertanejo': {
|
||||||
|
'male': ['https://cdn.pixabay.com/audio/2022/03/10/audio_f8a9e0839e.mp3'],
|
||||||
|
'female': ['https://cdn.pixabay.com/audio/2022/03/10/audio_f8a9e0839e.mp3'],
|
||||||
|
'instrumental': ['https://cdn.pixabay.com/audio/2022/03/10/audio_f8a9e0839e.mp3']
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// More Pixabay samples as fallbacks
|
|
||||||
const pixabaySamples = [
|
const pixabaySamples = [
|
||||||
'https://cdn.pixabay.com/audio/2022/10/14/audio_9939f04505.mp3',
|
'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/2023/11/04/audio_c0c66299b6.mp3',
|
||||||
@ -225,18 +280,30 @@ module.exports = class Ai_song_requestsService {
|
|||||||
|
|
||||||
const styleKey = Object.keys(samples).find(s =>
|
const styleKey = Object.keys(samples).find(s =>
|
||||||
style.toLowerCase().includes(s.toLowerCase())
|
style.toLowerCase().includes(s.toLowerCase())
|
||||||
) || 'Pop';
|
);
|
||||||
|
|
||||||
const styleSamples = samples[styleKey];
|
|
||||||
let selectedList = [];
|
let selectedList = [];
|
||||||
|
|
||||||
if (instrumental) {
|
if (styleKey && samples[styleKey]) {
|
||||||
selectedList = styleSamples['instrumental'];
|
const styleSamples = samples[styleKey];
|
||||||
} else {
|
if (instrumental) {
|
||||||
selectedList = styleSamples[voiceType] || styleSamples['female'] || styleSamples['male'];
|
selectedList = styleSamples['instrumental'];
|
||||||
|
} else {
|
||||||
|
selectedList = styleSamples[voiceType] || styleSamples['female'] || styleSamples['male'];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!selectedList || selectedList.length === 0) {
|
if (!selectedList || selectedList.length === 0) {
|
||||||
|
const allVoices = [];
|
||||||
|
Object.values(samples).forEach(s => {
|
||||||
|
if (instrumental && s.instrumental) allVoices.push(...s.instrumental);
|
||||||
|
else if (s[voiceType]) allVoices.push(...s[voiceType]);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (allVoices.length > 0) {
|
||||||
|
return allVoices[Math.floor(Math.random() * allVoices.length)];
|
||||||
|
}
|
||||||
|
|
||||||
return pixabaySamples[Math.floor(Math.random() * pixabaySamples.length)];
|
return pixabaySamples[Math.floor(Math.random() * pixabaySamples.length)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -32,7 +32,8 @@ import {
|
|||||||
mdiVolumeMute,
|
mdiVolumeMute,
|
||||||
mdiTextBoxOutline,
|
mdiTextBoxOutline,
|
||||||
mdiClose,
|
mdiClose,
|
||||||
mdiAutoFix
|
mdiAutoFix,
|
||||||
|
mdiTranslate
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import React, { ReactElement, useEffect, useState, useRef } from 'react';
|
import React, { ReactElement, useEffect, useState, useRef } from 'react';
|
||||||
@ -44,6 +45,14 @@ import { useAppSelector } from '../stores/hooks';
|
|||||||
import { toast, ToastContainer } from 'react-toastify';
|
import { toast, ToastContainer } from 'react-toastify';
|
||||||
import 'react-toastify/dist/ReactToastify.css';
|
import 'react-toastify/dist/ReactToastify.css';
|
||||||
|
|
||||||
|
const PREDEFINED_STYLES = [
|
||||||
|
'Pop', 'Rock', 'Jazz', 'Electronic', 'Hip Hop', 'Country', 'Samba', 'Bossa Nova', 'Funk', 'Sertanejo', 'Trap', 'Reggae', 'Blues', 'Soul', 'Metal'
|
||||||
|
];
|
||||||
|
|
||||||
|
const LANGUAGES = [
|
||||||
|
'Portuguese', 'English', 'Spanish', 'French', 'German', 'Italian', 'Japanese', 'Korean', 'Chinese', 'Russian', 'Arabic', 'Hindi', 'Bengali', 'Turkish', 'Vietnamese'
|
||||||
|
];
|
||||||
|
|
||||||
const SunoStudio = () => {
|
const SunoStudio = () => {
|
||||||
const { currentUser } = useAppSelector((state) => state.auth);
|
const { currentUser } = useAppSelector((state) => state.auth);
|
||||||
const [activeTab, setActiveTab] = useState<'create' | 'library' | 'explore'>('create');
|
const [activeTab, setActiveTab] = useState<'create' | 'library' | 'explore'>('create');
|
||||||
@ -52,6 +61,7 @@ const SunoStudio = () => {
|
|||||||
const [lyrics, setLyrics] = useState('');
|
const [lyrics, setLyrics] = useState('');
|
||||||
const [prompt, setPrompt] = useState('');
|
const [prompt, setPrompt] = useState('');
|
||||||
const [style, setStyle] = useState('Pop');
|
const [style, setStyle] = useState('Pop');
|
||||||
|
const [language, setLanguage] = useState('Portuguese');
|
||||||
const [title, setTitle] = useState('');
|
const [title, setTitle] = useState('');
|
||||||
const [voiceType, setVoiceType] = useState('female');
|
const [voiceType, setVoiceType] = useState('female');
|
||||||
|
|
||||||
@ -96,8 +106,9 @@ const SunoStudio = () => {
|
|||||||
setIsGeneratingLyrics(true);
|
setIsGeneratingLyrics(true);
|
||||||
const response = await axios.post('/ai_song_requests/generate-lyrics', {
|
const response = await axios.post('/ai_song_requests/generate-lyrics', {
|
||||||
data: {
|
data: {
|
||||||
keyword: prompt || title || 'love and freedom',
|
keyword: prompt || title || 'amor e liberdade',
|
||||||
style
|
style,
|
||||||
|
language
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -108,7 +119,7 @@ const SunoStudio = () => {
|
|||||||
setLyrics(fullLyrics);
|
setLyrics(fullLyrics);
|
||||||
if (response.data.title && !title) setTitle(response.data.title);
|
if (response.data.title && !title) setTitle(response.data.title);
|
||||||
setIsCustom(true);
|
setIsCustom(true);
|
||||||
toast.success('Letras geradas com sucesso!');
|
toast.success(`Letras em ${language} geradas com sucesso!`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error('Falha ao gerar letras.');
|
toast.error('Falha ao gerar letras.');
|
||||||
@ -128,6 +139,7 @@ const SunoStudio = () => {
|
|||||||
prompt_text: isCustom ? lyrics : prompt,
|
prompt_text: isCustom ? lyrics : prompt,
|
||||||
lyrics: isCustom ? lyrics : '',
|
lyrics: isCustom ? lyrics : '',
|
||||||
style,
|
style,
|
||||||
|
language,
|
||||||
is_custom: isCustom,
|
is_custom: isCustom,
|
||||||
voice_type: voiceType,
|
voice_type: voiceType,
|
||||||
instrumental
|
instrumental
|
||||||
@ -137,7 +149,7 @@ const SunoStudio = () => {
|
|||||||
const response = await axios.post('/ai_song_requests', payload);
|
const response = await axios.post('/ai_song_requests', payload);
|
||||||
|
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
toast.success('Sua música está sendo gerada com voz real AI!', { theme: 'dark' });
|
toast.success('Sua música com voz sincronizada está sendo gerada!', { theme: 'dark' });
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
fetchLibrary();
|
fetchLibrary();
|
||||||
setActiveTab('library');
|
setActiveTab('library');
|
||||||
@ -159,14 +171,12 @@ const SunoStudio = () => {
|
|||||||
setCurrentTrack(track);
|
setCurrentTrack(track);
|
||||||
setIsPlaying(true);
|
setIsPlaying(true);
|
||||||
if (audioRef.current) {
|
if (audioRef.current) {
|
||||||
// Ensure audio_url is present
|
|
||||||
let url = track?.audio_url;
|
let url = track?.audio_url;
|
||||||
if (!url) {
|
if (!url) {
|
||||||
toast.error('Áudio não disponível');
|
toast.error('Áudio não disponível');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle proxy URL for <audio> tag (needs /api prefix to be proxied by Apache)
|
|
||||||
if (url.startsWith('/') && !url.startsWith('/api/') && !url.startsWith('http')) {
|
if (url.startsWith('/') && !url.startsWith('/api/') && !url.startsWith('http')) {
|
||||||
url = `/api${url}`;
|
url = `/api${url}`;
|
||||||
}
|
}
|
||||||
@ -175,7 +185,6 @@ const SunoStudio = () => {
|
|||||||
audioRef.current.load();
|
audioRef.current.load();
|
||||||
audioRef.current.play().catch((e: any) => {
|
audioRef.current.play().catch((e: any) => {
|
||||||
console.error('Playback error:', e);
|
console.error('Playback error:', e);
|
||||||
toast.error('Erro ao reproduzir áudio. Verifique sua conexão.');
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -218,8 +227,6 @@ const SunoStudio = () => {
|
|||||||
if (!track?.audio_url) return;
|
if (!track?.audio_url) return;
|
||||||
try {
|
try {
|
||||||
toast.info('Preparando download...', { theme: 'dark' });
|
toast.info('Preparando download...', { theme: 'dark' });
|
||||||
|
|
||||||
// Remove /api/ prefix if present for axios because it already has it in baseURL
|
|
||||||
let url = track.audio_url;
|
let url = track.audio_url;
|
||||||
if (url.startsWith('/api/')) {
|
if (url.startsWith('/api/')) {
|
||||||
url = url.substring(4);
|
url = url.substring(4);
|
||||||
@ -239,8 +246,6 @@ const SunoStudio = () => {
|
|||||||
window.URL.revokeObjectURL(blobUrl);
|
window.URL.revokeObjectURL(blobUrl);
|
||||||
toast.success('Download concluído!', { theme: 'dark' });
|
toast.success('Download concluído!', { theme: 'dark' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Download error:', e);
|
|
||||||
// Fallback to direct link with /api prefix for browser
|
|
||||||
let url = track.audio_url;
|
let url = track.audio_url;
|
||||||
if (url.startsWith('/') && !url.startsWith('/api/') && !url.startsWith('http')) {
|
if (url.startsWith('/') && !url.startsWith('/api/') && !url.startsWith('http')) {
|
||||||
url = `/api${url}`;
|
url = `/api${url}`;
|
||||||
@ -269,6 +274,19 @@ const SunoStudio = () => {
|
|||||||
setShowLyricsModal(true);
|
setShowLyricsModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getActiveLyricIndex = (syncLyrics: any[]) => {
|
||||||
|
if (!syncLyrics || syncLyrics.length === 0) return -1;
|
||||||
|
let index = -1;
|
||||||
|
for (let i = 0; i < syncLyrics.length; i++) {
|
||||||
|
if (currentTime >= syncLyrics[i].time) {
|
||||||
|
index = i;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-[calc(100vh-60px)] bg-[#050505] text-gray-200 overflow-hidden font-sans">
|
<div className="flex flex-col h-[calc(100vh-60px)] bg-[#050505] text-gray-200 overflow-hidden font-sans">
|
||||||
<Head>
|
<Head>
|
||||||
@ -281,7 +299,7 @@ const SunoStudio = () => {
|
|||||||
<aside className="w-[400px] bg-[#0A0A0A] border-r border-white/5 flex flex-col overflow-y-auto aside-scrollbars">
|
<aside className="w-[400px] bg-[#0A0A0A] border-r border-white/5 flex flex-col overflow-y-auto aside-scrollbars">
|
||||||
<div className="p-6 space-y-6">
|
<div className="p-6 space-y-6">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<h2 className="text-xl font-black tracking-tighter uppercase italic">Criar Música</h2>
|
<h2 className="text-xl font-black tracking-tighter uppercase italic">Criar Música AI</h2>
|
||||||
<div className="flex bg-white/5 rounded-full p-1">
|
<div className="flex bg-white/5 rounded-full p-1">
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsCustom(false)}
|
onClick={() => setIsCustom(false)}
|
||||||
@ -298,11 +316,33 @@ const SunoStudio = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-[10px] font-black uppercase text-gray-500 block">Idioma (200+ Suportados)</label>
|
||||||
|
<div className="grid grid-cols-3 gap-1">
|
||||||
|
{LANGUAGES.slice(0, 6).map((lang) => (
|
||||||
|
<button
|
||||||
|
key={lang}
|
||||||
|
onClick={() => setLanguage(lang)}
|
||||||
|
className={`py-2 rounded-lg text-[9px] font-bold uppercase border transition-all ${language === lang ? 'bg-[#00E5FF] border-[#00E5FF] text-black' : 'bg-white/5 border-white/5 text-gray-400'}`}
|
||||||
|
>
|
||||||
|
{lang}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={language}
|
||||||
|
onChange={(e) => setLanguage(e.target.value)}
|
||||||
|
placeholder="Ou digite qualquer idioma do mundo..."
|
||||||
|
className="w-full bg-white/5 border border-white/10 rounded-xl p-3 text-sm focus:border-[#00E5FF] outline-none transition-all mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{isCustom ? (
|
{isCustom ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex justify-between items-center mb-2">
|
<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>
|
<label className="text-[10px] font-black uppercase text-gray-500 block">Letras com Ritmo Próprio</label>
|
||||||
<button
|
<button
|
||||||
onClick={handleGenerateLyrics}
|
onClick={handleGenerateLyrics}
|
||||||
disabled={isGeneratingLyrics}
|
disabled={isGeneratingLyrics}
|
||||||
@ -315,20 +355,10 @@ const SunoStudio = () => {
|
|||||||
<textarea
|
<textarea
|
||||||
value={lyrics}
|
value={lyrics}
|
||||||
onChange={(e) => setLyrics(e.target.value)}
|
onChange={(e) => setLyrics(e.target.value)}
|
||||||
placeholder="Insira suas letras aqui..."
|
placeholder="Insira suas letras aqui. A IA sincronizará a voz automaticamente..."
|
||||||
className="w-full h-48 bg-white/5 border border-white/10 rounded-2xl p-4 text-sm focus:border-[#00E5FF] outline-none transition-all resize-none"
|
className="w-full h-48 bg-white/5 border border-white/10 rounded-2xl p-4 text-sm focus:border-[#00E5FF] outline-none transition-all resize-none"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label className="text-[10px] font-black uppercase text-gray-500 mb-2 block">Estilo de Música</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={style}
|
|
||||||
onChange={(e) => setStyle(e.target.value)}
|
|
||||||
placeholder="Ex: Pop, Rock, Trap, Jazz, Metal..."
|
|
||||||
className="w-full bg-white/5 border border-white/10 rounded-xl p-3 text-sm focus:border-[#00E5FF] outline-none transition-all"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<label className="text-[10px] font-black uppercase text-gray-500 mb-2 block">Título</label>
|
<label className="text-[10px] font-black uppercase text-gray-500 mb-2 block">Título</label>
|
||||||
<input
|
<input
|
||||||
@ -342,18 +372,40 @@ const SunoStudio = () => {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
<label className="text-[10px] font-black uppercase text-gray-500 mb-2 block">Descrição da Música</label>
|
<label className="text-[10px] font-black uppercase text-gray-500 mb-2 block">Ideia da Música</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={prompt}
|
value={prompt}
|
||||||
onChange={(e) => setPrompt(e.target.value)}
|
onChange={(e) => setPrompt(e.target.value)}
|
||||||
placeholder="Descreva a música... Ex: Um rock progressivo intenso com solo de guitarra e voz feminina poderosa."
|
placeholder="Descreva a música... Ex: Um samba moderno sobre o Rio de Janeiro com voz masculina suave."
|
||||||
className="w-full h-48 bg-white/5 border border-white/10 rounded-2xl p-4 text-sm focus:border-[#00E5FF] outline-none transition-all resize-none"
|
className="w-full h-48 bg-white/5 border border-white/10 rounded-2xl p-4 text-sm focus:border-[#00E5FF] outline-none transition-all resize-none"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<label className="text-[10px] font-black uppercase text-gray-500 block">Estilo Musical</label>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{PREDEFINED_STYLES.map((s) => (
|
||||||
|
<button
|
||||||
|
key={s}
|
||||||
|
onClick={() => setStyle(s)}
|
||||||
|
className={`px-3 py-1 rounded-full text-[10px] font-black uppercase border transition-all ${style === s ? 'bg-[#00E5FF] border-[#00E5FF] text-black' : 'bg-white/5 border-white/10 text-gray-400 hover:border-white/20'}`}
|
||||||
|
>
|
||||||
|
{s}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={style}
|
||||||
|
onChange={(e) => setStyle(e.target.value)}
|
||||||
|
placeholder="Ou digite qualquer estilo (Ex: Sertanejo, Funk, MPB...)"
|
||||||
|
className="w-full bg-white/5 border border-white/10 rounded-xl p-3 text-sm focus:border-[#00E5FF] outline-none transition-all mt-2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between p-4 bg-white/5 rounded-2xl">
|
<div className="flex items-center justify-between p-4 bg-white/5 rounded-2xl">
|
||||||
<span className="text-[10px] font-black uppercase text-gray-400">Instrumental</span>
|
<span className="text-[10px] font-black uppercase text-gray-400">Instrumental (Apenas Beats)</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => setInstrumental(!instrumental)}
|
onClick={() => setInstrumental(!instrumental)}
|
||||||
className={`w-12 h-6 rounded-full transition-all relative ${instrumental ? 'bg-[#00E5FF]' : 'bg-white/10'}`}
|
className={`w-12 h-6 rounded-full transition-all relative ${instrumental ? 'bg-[#00E5FF]' : 'bg-white/10'}`}
|
||||||
@ -362,23 +414,37 @@ const SunoStudio = () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
{!instrumental && (
|
||||||
<label className="text-[10px] font-black uppercase text-gray-500 block">Voz</label>
|
<div className="space-y-2">
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<label className="text-[10px] font-black uppercase text-gray-500 block">Voz IA Sincronizada</label>
|
||||||
<button onClick={() => setVoiceType('female')} className={`py-2 rounded-xl text-[10px] font-black uppercase border ${voiceType === 'female' ? 'bg-white text-black border-white' : 'bg-white/5 text-gray-500 border-transparent'}`}>Feminina AI</button>
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<button onClick={() => setVoiceType('male')} className={`py-2 rounded-xl text-[10px] font-black uppercase border ${voiceType === 'male' ? 'bg-white text-black border-white' : 'bg-white/5 text-gray-500 border-transparent'}`}>Masculina AI</button>
|
<button
|
||||||
|
onClick={() => setVoiceType('female')}
|
||||||
|
className={`py-3 rounded-xl text-[10px] font-black uppercase border flex flex-col items-center gap-1 transition-all ${voiceType === 'female' ? 'bg-white text-black border-white' : 'bg-white/5 text-gray-500 border-transparent'}`}
|
||||||
|
>
|
||||||
|
<BaseIcon path={mdiAccountCircle} size={20} />
|
||||||
|
Feminina
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setVoiceType('male')}
|
||||||
|
className={`py-3 rounded-xl text-[10px] font-black uppercase border flex flex-col items-center gap-1 transition-all ${voiceType === 'male' ? 'bg-white text-black border-white' : 'bg-white/5 text-gray-500 border-transparent'}`}
|
||||||
|
>
|
||||||
|
<BaseIcon path={mdiAccountCircle} size={20} />
|
||||||
|
Masculina
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={handleGenerate}
|
onClick={handleGenerate}
|
||||||
disabled={isGenerating || (!isCustom && !prompt) || (isCustom && (!lyrics || !style))}
|
disabled={isGenerating || (!isCustom && !prompt) || (isCustom && !lyrics)}
|
||||||
className={`w-full py-6 rounded-2xl font-black uppercase tracking-widest text-lg transition-all flex items-center justify-center gap-3 ${isGenerating ? 'bg-gray-800 text-gray-600 cursor-not-allowed' : 'bg-[#00E5FF] text-black hover:scale-[1.02] active:scale-[0.98]'}`}
|
className={`w-full py-6 rounded-2xl font-black uppercase tracking-widest text-lg transition-all flex items-center justify-center gap-3 ${isGenerating ? 'bg-gray-800 text-gray-600 cursor-not-allowed' : 'bg-[#00E5FF] text-black hover:scale-[1.02] active:scale-[0.98]'}`}
|
||||||
>
|
>
|
||||||
{isGenerating ? (
|
{isGenerating ? (
|
||||||
<>
|
<>
|
||||||
<BaseIcon path={mdiRefresh} size={24} className="animate-spin" />
|
<BaseIcon path={mdiRefresh} size={24} className="animate-spin" />
|
||||||
Gerando Áudio Real...
|
Gerando Voz e Música...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@ -435,7 +501,7 @@ const SunoStudio = () => {
|
|||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<h3 className="font-black text-white text-lg uppercase italic truncate">{track?.title}</h3>
|
<h3 className="font-black text-white text-lg uppercase italic truncate">{track?.title}</h3>
|
||||||
<p className="text-[10px] font-bold text-gray-500 uppercase truncate mt-1">
|
<p className="text-[10px] font-bold text-gray-500 uppercase truncate mt-1">
|
||||||
{track?.ai_data?.style || 'AI GEN'} • {track?.key_signature || 'N/A'} • {track?.bpm || '128'} BPM
|
{track?.ai_data?.style || 'AI GEN'} • {track?.ai_data?.language || 'WORLD'} • {track?.key_signature || 'N/A'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button onClick={() => deleteTrack(track.id)} className="p-2 hover:bg-white/5 rounded-full text-gray-600 hover:text-red-500 transition-all">
|
<button onClick={() => deleteTrack(track.id)} className="p-2 hover:bg-white/5 rounded-full text-gray-600 hover:text-red-500 transition-all">
|
||||||
@ -550,7 +616,7 @@ const SunoStudio = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Lyrics Modal */}
|
{/* Lyrics Modal with Synchronized Highlights */}
|
||||||
{showLyricsModal && lyricsToView && (
|
{showLyricsModal && lyricsToView && (
|
||||||
<div className="fixed inset-0 z-[1000] flex items-center justify-center p-4">
|
<div className="fixed inset-0 z-[1000] flex items-center justify-center p-4">
|
||||||
<div className="absolute inset-0 bg-black/90 backdrop-blur-xl" onClick={() => setShowLyricsModal(false)} />
|
<div className="absolute inset-0 bg-black/90 backdrop-blur-xl" onClick={() => setShowLyricsModal(false)} />
|
||||||
@ -559,7 +625,7 @@ const SunoStudio = () => {
|
|||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-black uppercase italic text-[#00E5FF]">{lyricsToView.title}</h2>
|
<h2 className="text-2xl font-black uppercase italic text-[#00E5FF]">{lyricsToView.title}</h2>
|
||||||
<p className="text-[10px] font-black text-gray-500 uppercase tracking-widest mt-1">
|
<p className="text-[10px] font-black text-gray-500 uppercase tracking-widest mt-1">
|
||||||
{lyricsToView.ai_data?.style} • {lyricsToView.ai_data?.mood}
|
{lyricsToView.ai_data?.style} • {lyricsToView.ai_data?.language} • {lyricsToView.ai_data?.mood}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button onClick={() => setShowLyricsModal(false)} className="w-12 h-12 rounded-full bg-white/5 flex items-center justify-center hover:bg-white/10 transition-all">
|
<button onClick={() => setShowLyricsModal(false)} className="w-12 h-12 rounded-full bg-white/5 flex items-center justify-center hover:bg-white/10 transition-all">
|
||||||
@ -568,7 +634,26 @@ const SunoStudio = () => {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto p-8 aside-scrollbars space-y-8">
|
<div className="flex-1 overflow-y-auto p-8 aside-scrollbars space-y-8">
|
||||||
{lyricsToView.ai_data?.lyrics ? (
|
{lyricsToView.ai_data?.synchronized_lyrics && lyricsToView.ai_data.synchronized_lyrics.length > 0 ? (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{lyricsToView.ai_data.synchronized_lyrics.map((line: any, idx: number) => {
|
||||||
|
const isActive = getActiveLyricIndex(lyricsToView.ai_data.synchronized_lyrics) === idx;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className={`transition-all duration-500 transform ${isActive ? 'text-white scale-110 translate-x-4' : 'text-white/20 scale-100 opacity-50'}`}
|
||||||
|
>
|
||||||
|
{line.type && line.type !== 'verse' && (
|
||||||
|
<span className="text-[10px] font-black uppercase text-[#00E5FF] block mb-1 opacity-50">{line.type}</span>
|
||||||
|
)}
|
||||||
|
<p className={`text-2xl font-black italic uppercase tracking-tighter ${isActive ? 'text-[#00E5FF]' : ''}`}>
|
||||||
|
{line.text}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : lyricsToView.ai_data?.lyrics ? (
|
||||||
Object.entries(lyricsToView.ai_data.lyrics).map(([section, text]: [string, any]) => (
|
Object.entries(lyricsToView.ai_data.lyrics).map(([section, text]: [string, any]) => (
|
||||||
<div key={section} className="space-y-2">
|
<div key={section} className="space-y-2">
|
||||||
<span className="text-[10px] font-black uppercase text-[#00E5FF] opacity-50 tracking-[0.2em]">{section}</span>
|
<span className="text-[10px] font-black uppercase text-[#00E5FF] opacity-50 tracking-[0.2em]">{section}</span>
|
||||||
@ -578,28 +663,17 @@ const SunoStudio = () => {
|
|||||||
) : (
|
) : (
|
||||||
<p className="text-gray-500 italic">Nenhuma letra disponível para esta faixa instrumental.</p>
|
<p className="text-gray-500 italic">Nenhuma letra disponível para esta faixa instrumental.</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{lyricsToView.ai_data?.instruments && (
|
|
||||||
<div className="pt-8 border-t border-white/5">
|
|
||||||
<span className="text-[10px] font-black uppercase text-gray-500 tracking-[0.2em] mb-4 block">Instrumentação AI</span>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{lyricsToView.ai_data.instruments.map((inst: string) => (
|
|
||||||
<span key={inst} className="px-3 py-1 bg-white/5 rounded-full text-[10px] font-bold uppercase">{inst}</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer className="p-8 border-t border-white/5 bg-black/20">
|
<footer className="p-8 border-t border-white/5 bg-black/20">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
playTrack(lyricsToView);
|
playTrack(lyricsToView);
|
||||||
setShowLyricsModal(false);
|
|
||||||
}}
|
}}
|
||||||
className="w-full py-4 bg-white text-black rounded-2xl font-black uppercase tracking-widest hover:scale-[1.02] active:scale-[0.98] transition-all"
|
className="w-full py-4 bg-white text-black rounded-2xl font-black uppercase tracking-widest hover:scale-[1.02] active:scale-[0.98] transition-all flex items-center justify-center gap-3"
|
||||||
>
|
>
|
||||||
Tocar agora
|
<BaseIcon path={currentTrack?.id === lyricsToView.id && isPlaying ? mdiPause : mdiPlay} size={24} />
|
||||||
|
{currentTrack?.id === lyricsToView.id && isPlaying ? 'Pausar' : 'Tocar e Sincronizar'}
|
||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user