5
This commit is contained in:
parent
1fe82c2536
commit
7c8a152858
@ -24,34 +24,30 @@ router.get('/:lng/:ns.json', wrapAsync(async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Read English base file
|
// Read English base file
|
||||||
const enContent = JSON.parse(fs.readFileSync(enFilePath, 'utf-8'));
|
let enContent;
|
||||||
|
try {
|
||||||
|
enContent = JSON.parse(fs.readFileSync(enFilePath, 'utf-8'));
|
||||||
|
} catch (e) {
|
||||||
|
return res.status(500).json({ error: 'Failed to parse English base file' });
|
||||||
|
}
|
||||||
|
|
||||||
// Use AI to translate the content
|
// Use AI to translate the content
|
||||||
// We send the whole JSON and ask the AI to translate values while keeping keys
|
// We send the whole JSON and ask the AI to translate values while keeping keys
|
||||||
const prompt = `Translate the following JSON object values into ${lng} language. Keep the keys and the structure exactly as they are.
|
const prompt = `Translate the following JSON object values into ${lng} language. Keep the keys and the structure exactly as they are.
|
||||||
Only return the translated JSON object, nothing else.
|
Only return the translated JSON object, nothing else.
|
||||||
JSON: ${JSON.stringify(enContent, null, 2)}`;
|
JSON: ${JSON.stringify(enContent)}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const aiResponse = await LocalAIApi.createResponse({
|
const aiResponse = await LocalAIApi.createResponse({
|
||||||
input: [
|
input: [
|
||||||
{ role: 'system', content: 'You are a translation assistant. You translate JSON values while preserving keys.' },
|
{ role: 'system', content: 'You are a translation assistant. You translate JSON values while preserving keys. Return only valid JSON.' },
|
||||||
{ role: 'user', content: prompt }
|
{ role: 'user', content: prompt }
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
if (aiResponse.success) {
|
if (aiResponse.success) {
|
||||||
let translatedText = LocalAIApi.extractText(aiResponse);
|
|
||||||
|
|
||||||
// Clean up the response in case AI wrapped it in code blocks
|
|
||||||
if (translatedText.includes('```json')) {
|
|
||||||
translatedText = translatedText.split('```json')[1].split('```')[0].trim();
|
|
||||||
} else if (translatedText.includes('```')) {
|
|
||||||
translatedText = translatedText.split('```')[1].split('```')[0].trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const translatedJson = JSON.parse(translatedText);
|
const translatedJson = LocalAIApi.decodeJsonFromResponse(aiResponse);
|
||||||
|
|
||||||
// Optionally save the translated file to cache it for future use
|
// Optionally save the translated file to cache it for future use
|
||||||
const lngDir = path.join(frontendLocalesDir, lng);
|
const lngDir = path.join(frontendLocalesDir, lng);
|
||||||
@ -62,8 +58,8 @@ router.get('/:lng/:ns.json', wrapAsync(async (req, res) => {
|
|||||||
|
|
||||||
return res.status(200).json(translatedJson);
|
return res.status(200).json(translatedJson);
|
||||||
} catch (parseError) {
|
} catch (parseError) {
|
||||||
console.error('Failed to parse AI translation JSON:', parseError, translatedText);
|
console.error('Failed to parse AI translation JSON:', parseError);
|
||||||
return res.status(500).json({ error: 'Invalid AI response format', details: translatedText });
|
return res.status(500).json({ error: 'Invalid AI response format', details: parseError.message });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error('AI translation failed:', aiResponse);
|
console.error('AI translation failed:', aiResponse);
|
||||||
@ -75,4 +71,4 @@ router.get('/:lng/:ns.json', wrapAsync(async (req, res) => {
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@ -12,22 +12,48 @@ module.exports = class Ai_song_requestsService {
|
|||||||
static async create(data, currentUser) {
|
static async create(data, currentUser) {
|
||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
try {
|
try {
|
||||||
|
const isCustom = data.is_custom === true || data.is_custom === 'true';
|
||||||
|
const voiceType = data.voice_type || 'both'; // male, female, both
|
||||||
|
|
||||||
// 1. Get AI suggestions for the song
|
// 1. Get AI suggestions for the song
|
||||||
const prompt = `Create a song structure based on this idea: "${data.prompt_text}". Style: ${data.style || 'Pop'}.
|
let prompt = '';
|
||||||
Return ONLY a JSON object with:
|
if (isCustom) {
|
||||||
"title": "a creative song title",
|
prompt = `Based on these lyrics: "${data.lyrics}", and style: "${data.style || 'Pop'}".
|
||||||
"bpm": a number between 80-140,
|
Create a full song configuration.
|
||||||
"key": "a musical key",
|
Return ONLY a JSON object with:
|
||||||
"lyrics": {"verse1": "...", "chorus": "...", "verse2": "..."}`;
|
"title": "${data.title || 'a creative song title'}",
|
||||||
|
"bpm": a number between 80-140,
|
||||||
|
"key": "a musical key",
|
||||||
|
"description": "a short description of the vibe",
|
||||||
|
"tags": ["tag1", "tag2"],
|
||||||
|
"lyrics": {"verse1": "...", "chorus": "...", "verse2": "...", "outro": "..."}`;
|
||||||
|
} else {
|
||||||
|
prompt = `Create a full song based on this idea: "${data.prompt_text}". Style: ${data.style || 'Pop'}.
|
||||||
|
Return ONLY a JSON object with:
|
||||||
|
"title": "a creative song title",
|
||||||
|
"bpm": a number between 80-140,
|
||||||
|
"key": "a musical key",
|
||||||
|
"description": "a short description of the vibe",
|
||||||
|
"tags": ["tag1", "tag2"],
|
||||||
|
"lyrics": {"verse1": "...", "chorus": "...", "verse2": "...", "outro": "..."}`;
|
||||||
|
}
|
||||||
|
|
||||||
const aiResponse = await LocalAIApi.createResponse({
|
const aiResponse = await LocalAIApi.createResponse({
|
||||||
input: [
|
input: [
|
||||||
{ role: 'system', content: 'You are a professional music producer. Return only valid JSON.' },
|
{ role: 'system', content: 'You are a professional music producer and songwriter. You excel at creating Suno-style song metadata. Return only valid JSON.' },
|
||||||
{ role: 'user', content: prompt }
|
{ role: 'user', content: prompt }
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
let aiData = { title: data.title || 'New AI Song', bpm: 120, key: 'C Major' };
|
let aiData = {
|
||||||
|
title: data.title || 'AI Generated Hit',
|
||||||
|
bpm: 128,
|
||||||
|
key: 'C Major',
|
||||||
|
description: 'AI Generated track with professional vocals',
|
||||||
|
tags: ['AI', 'Studio', data.style || 'Pop'],
|
||||||
|
lyrics: { verse1: '...', chorus: '...' }
|
||||||
|
};
|
||||||
|
|
||||||
if (aiResponse.success) {
|
if (aiResponse.success) {
|
||||||
try {
|
try {
|
||||||
const decoded = LocalAIApi.decodeJsonFromResponse(aiResponse);
|
const decoded = LocalAIApi.decodeJsonFromResponse(aiResponse);
|
||||||
@ -40,43 +66,40 @@ module.exports = class Ai_song_requestsService {
|
|||||||
// 2. Create the Project
|
// 2. Create the Project
|
||||||
const project = await db.projects.create({
|
const project = await db.projects.create({
|
||||||
title: aiData.title,
|
title: aiData.title,
|
||||||
status: 'in_progress',
|
status: 'completed',
|
||||||
bpm: aiData.bpm,
|
bpm: aiData.bpm,
|
||||||
key_signature: aiData.key,
|
key_signature: aiData.key,
|
||||||
ownerId: currentUser.id,
|
ownerId: currentUser.id,
|
||||||
createdBy: currentUser.id
|
createdBy: currentUser.id
|
||||||
}, { transaction });
|
}, { transaction });
|
||||||
|
|
||||||
// 3. Create a Track for the beat
|
// 3. Create a Track for the AI Vocal + Beat (Merged like Suno)
|
||||||
const track = await db.tracks.create({
|
const track = await db.tracks.create({
|
||||||
name: 'Main Beat',
|
name: 'Full Mix (Vocals + Music)',
|
||||||
track_type: 'audio',
|
track_type: 'audio',
|
||||||
projectId: project.id,
|
projectId: project.id,
|
||||||
order_index: 0,
|
order_index: 0,
|
||||||
volume: 0.8,
|
volume: 1.0,
|
||||||
createdBy: currentUser.id
|
createdBy: currentUser.id
|
||||||
}, { transaction });
|
}, { transaction });
|
||||||
|
|
||||||
// 4. Create an Audio Clip (The "Real Beat")
|
// 4. Get simulated AI Vocal URL
|
||||||
// Using a royalty-free beat placeholder URL
|
const vocalUrl = this.getSimulatedMusicUrl(data.style || 'Pop', voiceType);
|
||||||
// In a real scenario, this would be a file generated or fetched from a music AI service
|
|
||||||
const beatUrl = this.getBeatUrlByStyle(data.style || 'Pop');
|
|
||||||
|
|
||||||
const audioClip = await db.audio_clips.create({
|
const audioClip = await db.audio_clips.create({
|
||||||
name: 'Drum Beat',
|
name: 'AI Generated Song',
|
||||||
trackId: track.id,
|
trackId: track.id,
|
||||||
start_bar: 0,
|
start_bar: 0,
|
||||||
length_bars: 16,
|
length_bars: 32,
|
||||||
gain: 1.0,
|
gain: 1.0,
|
||||||
createdBy: currentUser.id
|
createdBy: currentUser.id
|
||||||
}, { transaction });
|
}, { transaction });
|
||||||
|
|
||||||
// We should ideally create a File entry for this URL, but for the prototype
|
// Save lyrics to project or a separate field if we had it.
|
||||||
// we'll handle the URL in the frontend or add it to a specific field if available.
|
// For now, let's put them in the AI request details.
|
||||||
// Since 'audio_file' is a scope-based relation, we'll need to handle it properly.
|
|
||||||
|
// Update the AI song request with the project ID and enhanced data
|
||||||
// Update the AI song request with the project ID
|
const aiRequest = await Ai_song_requestsDBApi.create(
|
||||||
await Ai_song_requestsDBApi.create(
|
|
||||||
{
|
{
|
||||||
...data,
|
...data,
|
||||||
title: aiData.title,
|
title: aiData.title,
|
||||||
@ -84,7 +107,9 @@ module.exports = class Ai_song_requestsService {
|
|||||||
projectId: project.id,
|
projectId: project.id,
|
||||||
target_bpm: aiData.bpm,
|
target_bpm: aiData.bpm,
|
||||||
key_signature: aiData.key,
|
key_signature: aiData.key,
|
||||||
completed_at: new Date()
|
completed_at: new Date(),
|
||||||
|
// We'll store the AI metadata in prompt_text or a JSON field if we had it
|
||||||
|
// For now, let's just make sure it returns the project
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
currentUser,
|
currentUser,
|
||||||
@ -93,22 +118,47 @@ module.exports = class Ai_song_requestsService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await transaction.commit();
|
await transaction.commit();
|
||||||
return project;
|
|
||||||
|
// Return the project with AI metadata attached for the frontend
|
||||||
|
return {
|
||||||
|
...project.get({ plain: true }),
|
||||||
|
ai_data: aiData,
|
||||||
|
audio_url: vocalUrl
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (transaction) await transaction.rollback();
|
if (transaction) await transaction.rollback();
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
static getBeatUrlByStyle(style) {
|
static getSimulatedMusicUrl(style, voiceType) {
|
||||||
const beats = {
|
// Mapping of styles and voices to high-quality placeholder tracks that include vocals
|
||||||
'Pop': 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3',
|
// These would be replaced by actual AI music generation API calls (like Suno/Udio/Replicate)
|
||||||
'Rock': 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3',
|
const samples = {
|
||||||
'Hip-Hop': 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-3.mp3',
|
'Pop': {
|
||||||
'Electronic': 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-4.mp3',
|
'male': 'https://cdn.pixabay.com/audio/2022/10/14/audio_9939f04505.mp3', // Uplifting Pop
|
||||||
'Jazz': 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-8.mp3'
|
'female': 'https://cdn.pixabay.com/audio/2023/10/24/audio_333458421d.mp3', // Pop Female Vocal
|
||||||
|
'both': 'https://cdn.pixabay.com/audio/2022/10/14/audio_9939f04505.mp3'
|
||||||
|
},
|
||||||
|
'Rock': {
|
||||||
|
'male': 'https://cdn.pixabay.com/audio/2022/01/21/audio_24859f0359.mp3', // Rock Energetic
|
||||||
|
'female': 'https://cdn.pixabay.com/audio/2023/06/07/audio_4d38c62c2f.mp3', // Rock Female
|
||||||
|
'both': 'https://cdn.pixabay.com/audio/2022/01/21/audio_24859f0359.mp3'
|
||||||
|
},
|
||||||
|
'Hip-Hop': {
|
||||||
|
'male': 'https://cdn.pixabay.com/audio/2022/03/10/audio_f8a9e0839e.mp3', // Hip Hop Male
|
||||||
|
'female': 'https://cdn.pixabay.com/audio/2024/02/14/audio_108573ec60.mp3', // Chill Lofi Female
|
||||||
|
'both': 'https://cdn.pixabay.com/audio/2022/03/10/audio_f8a9e0839e.mp3'
|
||||||
|
},
|
||||||
|
'Electronic': {
|
||||||
|
'male': 'https://cdn.pixabay.com/audio/2021/11/24/audio_83a544605b.mp3', // EDM
|
||||||
|
'female': 'https://cdn.pixabay.com/audio/2023/01/15/audio_812384668f.mp3', // Techno Female
|
||||||
|
'both': 'https://cdn.pixabay.com/audio/2021/11/24/audio_83a544605b.mp3'
|
||||||
|
}
|
||||||
};
|
};
|
||||||
return beats[style] || beats['Pop'];
|
|
||||||
|
const styleSamples = samples[style] || samples['Pop'];
|
||||||
|
return styleSamples[voiceType] || styleSamples['both'];
|
||||||
}
|
}
|
||||||
|
|
||||||
static async bulkImport(req, res, sendInvitationEmails = true, host) {
|
static async bulkImport(req, res, sendInvitationEmails = true, host) {
|
||||||
|
|||||||
@ -5,111 +5,109 @@ import {
|
|||||||
mdiPlay,
|
mdiPlay,
|
||||||
mdiPause,
|
mdiPause,
|
||||||
mdiStop,
|
mdiStop,
|
||||||
mdiPiano,
|
|
||||||
mdiMicrophone,
|
mdiMicrophone,
|
||||||
mdiTuneVariant,
|
|
||||||
mdiDownload,
|
mdiDownload,
|
||||||
mdiRefresh,
|
mdiRefresh,
|
||||||
mdiDelete
|
mdiDelete,
|
||||||
|
mdiMenu,
|
||||||
|
mdiChevronDown,
|
||||||
|
mdiCreation,
|
||||||
|
mdiPlaylistMusic,
|
||||||
|
mdiLibrary,
|
||||||
|
mdiAccountCircle,
|
||||||
|
mdiContentCopy,
|
||||||
|
mdiVolumeHigh,
|
||||||
|
mdiHeart,
|
||||||
|
mdiShare
|
||||||
} 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';
|
||||||
import CardBox from '../components/CardBox';
|
import CardBox from '../components/CardBox';
|
||||||
import LayoutAuthenticated from '../layouts/Authenticated';
|
import LayoutAuthenticated from '../layouts/Authenticated';
|
||||||
import SectionMain from '../components/SectionMain';
|
import SectionMain from '../components/SectionMain';
|
||||||
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton';
|
|
||||||
import { getPageTitle } from '../config';
|
import { getPageTitle } from '../config';
|
||||||
import BaseIcon from '../components/BaseIcon';
|
import BaseIcon from '../components/BaseIcon';
|
||||||
import BaseButton from '../components/BaseButton';
|
import BaseButton from '../components/BaseButton';
|
||||||
import FormField from '../components/FormField';
|
|
||||||
import BaseDivider from '../components/BaseDivider';
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { useAppDispatch, useAppSelector } from '../stores/hooks';
|
import { useAppSelector } from '../stores/hooks';
|
||||||
import { toast, ToastContainer } from 'react-toastify';
|
import { toast, ToastContainer } from 'react-toastify';
|
||||||
|
|
||||||
const Studio = () => {
|
const SunoStudio = () => {
|
||||||
const { currentUser } = useAppSelector((state) => state.auth);
|
const { currentUser } = useAppSelector((state) => state.auth);
|
||||||
|
|
||||||
// State
|
// Create State
|
||||||
|
const [isCustom, setIsCustom] = useState(false);
|
||||||
|
const [lyrics, setLyrics] = useState('');
|
||||||
const [prompt, setPrompt] = useState('');
|
const [prompt, setPrompt] = useState('');
|
||||||
const [style, setStyle] = useState('Pop');
|
const [style, setStyle] = useState('Pop');
|
||||||
const [projects, setProjects] = useState([]);
|
const [title, setTitle] = useState('');
|
||||||
const [instruments, setInstruments] = useState([]);
|
const [voiceType, setVoiceType] = useState('female');
|
||||||
|
|
||||||
|
// Library State
|
||||||
|
const [library, setLibrary] = useState<any[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [generating, setGenerating] = useState(false);
|
const [generating, setGenerating] = useState(false);
|
||||||
const [currentProject, setCurrentProject] = useState<any>(null);
|
|
||||||
|
|
||||||
// Audio Playback State
|
// Player State
|
||||||
|
const [currentTrack, setCurrentTrack] = useState<any>(null);
|
||||||
const [isPlaying, setIsPlaying] = useState(false);
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
const [audioUrl, setAudioUrl] = useState('');
|
|
||||||
const audioRef = useRef<HTMLAudioElement | null>(null);
|
const audioRef = useRef<HTMLAudioElement | null>(null);
|
||||||
|
const [progress, setProgress] = useState(0);
|
||||||
// Recording State
|
const [showLyrics, setShowLyrics] = useState(false);
|
||||||
const [isRecording, setIsRecording] = useState(false);
|
|
||||||
const [recordedBlob, setRecordedBlob] = useState<Blob | null>(null);
|
|
||||||
const [recordedUrl, setRecordedUrl] = useState('');
|
|
||||||
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
|
|
||||||
const audioChunksRef = useRef<Blob[]>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchStudioData();
|
fetchLibrary();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchStudioData = async () => {
|
const fetchLibrary = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const [projRes, instRes] = await Promise.all([
|
const response = await axios.get('/projects?limit=20');
|
||||||
axios.get('/projects?limit=5'),
|
// We'll simulate library by fetching projects
|
||||||
axios.get('/instruments?limit=8')
|
setLibrary(response.data.rows || []);
|
||||||
]);
|
|
||||||
setProjects(projRes.data.rows || []);
|
|
||||||
setInstruments(instRes.data.rows || []);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching studio data:', error);
|
console.error('Error fetching library:', error);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getBeatUrlByStyle = (style: string) => {
|
const handleCreate = async () => {
|
||||||
const beats: any = {
|
if (!isCustom && !prompt) {
|
||||||
'Pop': 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3',
|
|
||||||
'Rock': 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3',
|
|
||||||
'Hip-Hop': 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-3.mp3',
|
|
||||||
'Electronic': 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-4.mp3',
|
|
||||||
'Jazz': 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-8.mp3'
|
|
||||||
};
|
|
||||||
return beats[style] || beats['Pop'];
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAiGenerate = async () => {
|
|
||||||
if (!prompt) {
|
|
||||||
toast.error('Descreva sua ideia para a música');
|
toast.error('Descreva sua ideia para a música');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (isCustom && !lyrics) {
|
||||||
|
toast.error('Insira a letra da música');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setGenerating(true);
|
setGenerating(true);
|
||||||
const response = await axios.post('/ai_song_requests', {
|
const response = await axios.post('/ai_song_requests', {
|
||||||
data: {
|
data: {
|
||||||
prompt_text: prompt,
|
prompt_text: prompt,
|
||||||
|
lyrics: lyrics,
|
||||||
style: style,
|
style: style,
|
||||||
title: `AI Project - ${style}`
|
title: title || `AI Project - ${style}`,
|
||||||
|
is_custom: isCustom,
|
||||||
|
voice_type: voiceType
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
toast.success('Música e batida geradas com sucesso!');
|
const newProject = response.data;
|
||||||
|
toast.success('Sua música está sendo gerada!');
|
||||||
|
|
||||||
// Fetch latest projects to get the new one
|
// Update library
|
||||||
const projRes = await axios.get('/projects?limit=1');
|
setLibrary([newProject, ...library]);
|
||||||
const newProject = projRes.data.rows[0];
|
|
||||||
setCurrentProject(newProject);
|
|
||||||
|
|
||||||
// Set audio URL for the beat
|
// Auto-play the new track
|
||||||
const beat = getBeatUrlByStyle(style);
|
playTrack(newProject);
|
||||||
setAudioUrl(beat);
|
|
||||||
|
|
||||||
fetchStudioData();
|
// Reset form
|
||||||
|
setPrompt('');
|
||||||
|
setLyrics('');
|
||||||
|
setTitle('');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error generating song:', error);
|
console.error('Error generating song:', error);
|
||||||
toast.error('Erro ao gerar música. Tente novamente.');
|
toast.error('Erro ao gerar música. Tente novamente.');
|
||||||
@ -118,9 +116,20 @@ const Studio = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const playTrack = (track: any) => {
|
||||||
|
setCurrentTrack(track);
|
||||||
|
setIsPlaying(true);
|
||||||
|
// Simulated audio URL if not present in track
|
||||||
|
const audioUrl = track.audio_url || 'https://cdn.pixabay.com/audio/2023/10/24/audio_333458421d.mp3';
|
||||||
|
|
||||||
|
if (audioRef.current) {
|
||||||
|
audioRef.current.src = audioUrl;
|
||||||
|
audioRef.current.play();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const togglePlayback = () => {
|
const togglePlayback = () => {
|
||||||
if (!audioRef.current) return;
|
if (!audioRef.current) return;
|
||||||
|
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
audioRef.current.pause();
|
audioRef.current.pause();
|
||||||
} else {
|
} else {
|
||||||
@ -129,352 +138,315 @@ const Studio = () => {
|
|||||||
setIsPlaying(!isPlaying);
|
setIsPlaying(!isPlaying);
|
||||||
};
|
};
|
||||||
|
|
||||||
const startRecording = async () => {
|
const handleTimeUpdate = () => {
|
||||||
try {
|
if (audioRef.current) {
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
const p = (audioRef.current.currentTime / audioRef.current.duration) * 100;
|
||||||
const mediaRecorder = new MediaRecorder(stream);
|
setProgress(p);
|
||||||
mediaRecorderRef.current = mediaRecorder;
|
|
||||||
audioChunksRef.current = [];
|
|
||||||
|
|
||||||
mediaRecorder.ondataavailable = (event) => {
|
|
||||||
if (event.data.size > 0) {
|
|
||||||
audioChunksRef.current.push(event.data);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
mediaRecorder.onstop = () => {
|
|
||||||
const audioBlob = new Blob(audioChunksRef.current, { type: 'audio/wav' });
|
|
||||||
setRecordedBlob(audioBlob);
|
|
||||||
setRecordedUrl(URL.createObjectURL(audioBlob));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Start playback of beat when recording starts
|
|
||||||
if (audioRef.current) {
|
|
||||||
audioRef.current.currentTime = 0;
|
|
||||||
audioRef.current.play();
|
|
||||||
setIsPlaying(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
mediaRecorder.start();
|
|
||||||
setIsRecording(true);
|
|
||||||
toast.info('Gravando...');
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Microphone access denied:', err);
|
|
||||||
toast.error('Acesso ao microfone negado.');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const stopRecording = () => {
|
const handleProgressChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
if (mediaRecorderRef.current && isRecording) {
|
if (audioRef.current) {
|
||||||
mediaRecorderRef.current.stop();
|
const newTime = (parseFloat(e.target.value) / 100) * audioRef.current.duration;
|
||||||
setIsRecording(false);
|
audioRef.current.currentTime = newTime;
|
||||||
|
setProgress(parseFloat(e.target.value));
|
||||||
// Stop beat playback
|
|
||||||
if (audioRef.current) {
|
|
||||||
audioRef.current.pause();
|
|
||||||
setIsPlaying(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.success('Gravação finalizada!');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex h-screen bg-black text-gray-200 overflow-hidden font-sans">
|
||||||
<Head>
|
<Head>
|
||||||
<title>{getPageTitle('Studio Musical')}</title>
|
<title>{getPageTitle('AI Music Studio - Studio')}</title>
|
||||||
</Head>
|
</Head>
|
||||||
<SectionMain>
|
|
||||||
<SectionTitleLineWithButton icon={mdiMusic} title='Studio Musical' main>
|
{/* Left Sidebar - Create Section */}
|
||||||
<div className="flex space-x-2">
|
<aside className="w-80 bg-[#121212] border-r border-white/5 flex flex-col p-4 overflow-y-auto aside-scrollbars">
|
||||||
<BaseButton
|
<div className="flex items-center space-x-2 mb-8 px-2">
|
||||||
label="Novo Projeto"
|
<div className="w-8 h-8 bg-[#00E5FF] rounded-lg flex items-center justify-center">
|
||||||
color="info"
|
<BaseIcon path={mdiMusic} className="text-black" />
|
||||||
icon={mdiPlus}
|
|
||||||
outline
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</SectionTitleLineWithButton>
|
<span className="text-xl font-black text-white tracking-tighter uppercase italic">AI STUDIO</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
<div className="space-y-6">
|
||||||
{/* Main Area */}
|
<div className="flex items-center justify-between px-2">
|
||||||
<div className="lg:col-span-2 space-y-6">
|
<h2 className="text-sm font-black text-white uppercase tracking-widest">CRIAR MÚSICA</h2>
|
||||||
{/* AI Generator Section */}
|
<div className="flex items-center space-x-2">
|
||||||
{!currentProject ? (
|
<span className="text-[10px] font-bold text-gray-500 uppercase tracking-tighter">Modo Custom</span>
|
||||||
<CardBox className="bg-gradient-to-br from-gray-900 to-indigo-900 border-none shadow-2xl overflow-hidden relative min-h-[400px] flex flex-col justify-center">
|
<button
|
||||||
<div className="absolute top-0 right-0 p-4 opacity-10">
|
onClick={() => setIsCustom(!isCustom)}
|
||||||
<BaseIcon path={mdiRobotOutline} size={200} />
|
className={`w-8 h-4 rounded-full transition-colors relative ${isCustom ? 'bg-[#00E5FF]' : 'bg-gray-700'}`}
|
||||||
</div>
|
>
|
||||||
<div className="relative z-10 p-4 md:p-8">
|
<div className={`absolute top-0.5 w-3 h-3 bg-white rounded-full transition-all ${isCustom ? 'left-4.5' : 'left-0.5'}`} />
|
||||||
<h2 className="text-3xl font-black text-white mb-2 flex items-center tracking-tighter">
|
</button>
|
||||||
<BaseIcon path={mdiRobotOutline} className="mr-3 text-[#00E5FF]" size={40} />
|
|
||||||
AI SONGWRITER
|
|
||||||
</h2>
|
|
||||||
<p className="text-indigo-200 mb-8 text-lg font-medium">Descreva sua ideia, escolha um estilo e deixe a IA criar a base do seu próximo hit com batidas reais.</p>
|
|
||||||
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="group">
|
|
||||||
<label className="text-xs font-black text-indigo-300 uppercase mb-2 block tracking-widest">Sua ideia para a música</label>
|
|
||||||
<textarea
|
|
||||||
className="w-full bg-black/40 border-indigo-500/30 text-white rounded-2xl p-5 focus:ring-2 focus:ring-[#00E5FF] transition-all outline-none text-lg"
|
|
||||||
placeholder="Ex: Uma música romântica sobre uma noite de verão..."
|
|
||||||
rows={3}
|
|
||||||
value={prompt}
|
|
||||||
onChange={(e) => setPrompt(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4">
|
|
||||||
<div className="flex-1">
|
|
||||||
<label className="text-xs font-black text-indigo-300 uppercase mb-2 block tracking-widest">Estilo Musical</label>
|
|
||||||
<select
|
|
||||||
className="w-full bg-black/40 border-indigo-500/30 text-white rounded-2xl p-4 focus:ring-2 focus:ring-[#00E5FF] outline-none appearance-none"
|
|
||||||
value={style}
|
|
||||||
onChange={(e) => setStyle(e.target.value)}
|
|
||||||
>
|
|
||||||
<option>Pop</option>
|
|
||||||
<option>Rock</option>
|
|
||||||
<option>Hip-Hop</option>
|
|
||||||
<option>Jazz</option>
|
|
||||||
<option>Electronic</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-end">
|
|
||||||
<BaseButton
|
|
||||||
label={generating ? "COMPONDO..." : "GERAR MÚSICA & BATIDA"}
|
|
||||||
color="info"
|
|
||||||
className="w-full md:w-auto px-10 py-4 font-black rounded-2xl shadow-[0_0_20px_rgba(0,229,255,0.4)] hover:scale-105 transition-transform"
|
|
||||||
disabled={generating}
|
|
||||||
onClick={handleAiGenerate}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardBox>
|
|
||||||
) : (
|
|
||||||
/* Active Studio / Recording Section */
|
|
||||||
<CardBox className="bg-gray-900 border-indigo-500/20 shadow-2xl relative overflow-hidden">
|
|
||||||
<div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-[#00E5FF] via-[#BB86FC] to-[#00E5FF] animate-pulse"></div>
|
|
||||||
|
|
||||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-8 gap-4">
|
|
||||||
<div>
|
|
||||||
<span className="text-[10px] font-black bg-indigo-500 text-white px-2 py-0.5 rounded uppercase tracking-tighter mb-2 inline-block">Projeto Ativo</span>
|
|
||||||
<h2 className="text-2xl font-black text-white uppercase italic tracking-tighter">{currentProject.title}</h2>
|
|
||||||
<p className="text-gray-500 text-sm font-bold uppercase tracking-widest">{style} • {currentProject.bpm} BPM</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<BaseButton
|
|
||||||
icon={mdiRefresh}
|
|
||||||
color="white"
|
|
||||||
outline
|
|
||||||
small
|
|
||||||
onClick={() => {
|
|
||||||
setCurrentProject(null);
|
|
||||||
setRecordedUrl('');
|
|
||||||
setRecordedBlob(null);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<BaseButton icon={mdiDownload} color="white" outline small />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
|
||||||
{/* Playback Controls */}
|
|
||||||
<div className="bg-black/40 rounded-3xl p-6 border border-white/5">
|
|
||||||
<div className="flex items-center justify-between mb-6">
|
|
||||||
<h3 className="text-xs font-black text-gray-500 uppercase tracking-widest flex items-center">
|
|
||||||
<BaseIcon path={mdiPlay} size={14} className="mr-1" /> PLAYBACK (BATIDA)
|
|
||||||
</h3>
|
|
||||||
<span className="text-[10px] font-bold text-[#00E5FF]">REAL BEAT ENGINE</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-center py-8">
|
|
||||||
<button
|
|
||||||
onClick={togglePlayback}
|
|
||||||
className="w-20 h-20 rounded-full bg-white flex items-center justify-center text-black hover:scale-110 transition-transform shadow-xl"
|
|
||||||
>
|
|
||||||
<BaseIcon path={isPlaying ? mdiPause : mdiPlay} size={48} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<audio
|
|
||||||
ref={audioRef}
|
|
||||||
src={audioUrl}
|
|
||||||
onEnded={() => setIsPlaying(false)}
|
|
||||||
className="hidden"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="text-center text-xs font-black text-indigo-400 uppercase tracking-widest animate-pulse">
|
|
||||||
{isPlaying ? "REPRODUZINDO ARRANJO..." : "BATIDA CARREGADA"}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Recording Controls */}
|
|
||||||
<div className={`rounded-3xl p-6 border transition-all duration-500 ${isRecording ? 'bg-red-900/20 border-red-500' : 'bg-black/40 border-white/5'}`}>
|
|
||||||
<div className="flex items-center justify-between mb-6">
|
|
||||||
<h3 className="text-xs font-black text-gray-500 uppercase tracking-widest flex items-center">
|
|
||||||
<BaseIcon path={mdiMicrophone} size={14} className="mr-1" /> GRAVAÇÃO DE VOZ
|
|
||||||
</h3>
|
|
||||||
{isRecording && <span className="flex h-2 w-2 rounded-full bg-red-500 animate-ping"></span>}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-center py-8">
|
|
||||||
{!isRecording ? (
|
|
||||||
<button
|
|
||||||
onClick={startRecording}
|
|
||||||
className="w-20 h-20 rounded-full bg-red-600 flex items-center justify-center text-white hover:bg-red-500 transition-all shadow-lg group"
|
|
||||||
>
|
|
||||||
<BaseIcon path={mdiMicrophone} size={40} className="group-hover:scale-110 transition-transform" />
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
onClick={stopRecording}
|
|
||||||
className="w-20 h-20 rounded-full bg-white flex items-center justify-center text-red-600 animate-pulse shadow-xl"
|
|
||||||
>
|
|
||||||
<BaseIcon path={mdiStop} size={40} />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-center text-xs font-black uppercase tracking-widest">
|
|
||||||
{isRecording ? (
|
|
||||||
<span className="text-red-500">GRAVANDO POR CIMA DA BATIDA...</span>
|
|
||||||
) : recordedUrl ? (
|
|
||||||
<div className="flex flex-col items-center space-y-3">
|
|
||||||
<span className="text-green-500">GRAVAÇÃO CONCLUÍDA!</span>
|
|
||||||
<audio src={recordedUrl} controls className="h-8 w-full max-w-[200px]" />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<span className="text-gray-600">PRONTO PARA GRAVAR</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{recordedUrl && (
|
|
||||||
<div className="mt-8 p-6 bg-indigo-900/20 border border-indigo-500/30 rounded-3xl flex flex-col md:flex-row items-center justify-between gap-4">
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<div className="w-12 h-12 bg-indigo-500 rounded-2xl flex items-center justify-center">
|
|
||||||
<BaseIcon path={mdiMusic} className="text-white" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="font-black text-white uppercase italic">Mixagem Final</div>
|
|
||||||
<div className="text-xs text-indigo-300 font-bold uppercase tracking-tighter">Voz + Batida {style}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex space-x-3 w-full md:w-auto">
|
|
||||||
<BaseButton label="Baixar Mix" color="info" icon={mdiDownload} className="flex-1 md:flex-none font-black rounded-xl" />
|
|
||||||
<BaseButton icon={mdiDelete} color="danger" outline onClick={() => { setRecordedUrl(''); setRecordedBlob(null); }} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardBox>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
<CardBox title="Projetos Recentes" icon={mdiMusic}>
|
|
||||||
{projects.length > 0 ? (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{projects.map((project: any) => (
|
|
||||||
<div key={project.id} className="flex items-center justify-between p-3 bg-gray-800/50 rounded-xl hover:bg-gray-800 transition-colors cursor-pointer group">
|
|
||||||
<div className="flex items-center space-x-3">
|
|
||||||
<div className="w-10 h-10 bg-indigo-900 rounded-lg flex items-center justify-center">
|
|
||||||
<BaseIcon path={mdiMusic} className="text-indigo-400" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="font-bold text-sm">{project.title || 'Untitled Project'}</div>
|
|
||||||
<div className="text-xs text-gray-500">{project.genre?.name || style} • {project.bpm} BPM</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<BaseIcon path={mdiPlay} className="text-gray-600 group-hover:text-[#00E5FF] transition-colors" />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="text-center py-8 text-gray-500">
|
|
||||||
<p>Nenhum projeto ainda. Comece gerando um!</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardBox>
|
|
||||||
|
|
||||||
<CardBox title="Seus Instrumentos" icon={mdiPiano}>
|
|
||||||
<div className="grid grid-cols-2 gap-3">
|
|
||||||
{instruments.map((inst: any) => (
|
|
||||||
<div key={inst.id} className="p-3 bg-gray-800/50 rounded-xl flex flex-col items-center text-center hover:bg-gray-800 transition-colors cursor-pointer border border-transparent hover:border-indigo-500/50">
|
|
||||||
<BaseIcon path={mdiPiano} className="text-[#00E5FF] mb-2" />
|
|
||||||
<span className="text-xs font-black uppercase tracking-tighter">{inst.name}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<div className="p-3 bg-gray-900 border border-dashed border-gray-700 rounded-xl flex flex-col items-center text-center justify-center text-gray-500 hover:text-white hover:border-white transition-all cursor-pointer">
|
|
||||||
<BaseIcon path={mdiPlus} size={20} />
|
|
||||||
<span className="text-[10px] uppercase font-black mt-1">Adicionar</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardBox>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Sidebar */}
|
<div className="space-y-4">
|
||||||
<div className="space-y-6">
|
{isCustom ? (
|
||||||
<CardBox title="Status do Studio" icon={mdiTuneVariant}>
|
<div className="space-y-3 animate-fade-in">
|
||||||
<div className="space-y-6">
|
<label className="text-[10px] font-black text-gray-500 uppercase px-2">Letra da Música</label>
|
||||||
<div>
|
<textarea
|
||||||
<div className="flex justify-between text-xs font-black uppercase tracking-widest text-gray-500 mb-2">
|
className="w-full bg-[#1e1e1e] border-none text-sm text-white rounded-xl p-4 focus:ring-1 focus:ring-[#00E5FF] transition-all outline-none resize-none"
|
||||||
<span>Volume Master</span>
|
placeholder="Cole sua letra aqui..."
|
||||||
<span>85%</span>
|
rows={8}
|
||||||
</div>
|
value={lyrics}
|
||||||
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
|
onChange={(e) => setLyrics(e.target.value)}
|
||||||
<div className="h-full bg-gradient-to-r from-[#00E5FF] to-[#BB86FC] w-[85%]"></div>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4 text-center">
|
|
||||||
<div className="bg-gray-800 p-4 rounded-2xl border border-gray-700 shadow-lg">
|
|
||||||
<div className="text-2xl font-black text-white">{currentProject?.bpm || 128}</div>
|
|
||||||
<div className="text-[10px] text-gray-500 uppercase font-black tracking-widest">BPM</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-gray-800 p-4 rounded-2xl border border-gray-700 shadow-lg">
|
|
||||||
<div className="text-2xl font-black text-white">4/4</div>
|
|
||||||
<div className="text-[10px] text-gray-500 uppercase font-black tracking-widest">Tempo</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<BaseDivider />
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
<h4 className="text-xs font-black text-gray-500 uppercase tracking-widest">Efeitos Ativos</h4>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{['Reverb AI', 'Compressor', 'Auto-Tune'].map(fx => (
|
|
||||||
<span key={fx} className="px-3 py-1 bg-indigo-900/30 text-indigo-400 border border-indigo-900 rounded-full text-[10px] font-black uppercase italic tracking-tighter">
|
|
||||||
{fx}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</CardBox>
|
) : (
|
||||||
|
<div className="space-y-3 animate-fade-in">
|
||||||
<CardBox className="bg-gradient-to-br from-[#00E5FF] to-[#BB86FC] p-0 border-none overflow-hidden shadow-2xl group cursor-pointer">
|
<label className="text-[10px] font-black text-gray-500 uppercase px-2">Descrição da Música</label>
|
||||||
<div className="p-6 text-black relative">
|
<textarea
|
||||||
<div className="absolute top-0 right-0 p-4 opacity-10 group-hover:scale-125 transition-transform duration-700">
|
className="w-full bg-[#1e1e1e] border-none text-sm text-white rounded-xl p-4 focus:ring-1 focus:ring-[#00E5FF] transition-all outline-none resize-none"
|
||||||
<BaseIcon path={mdiRobotOutline} size={100} />
|
placeholder="Ex: Uma música pop animada sobre uma aventura no espaço com vocais femininos..."
|
||||||
</div>
|
rows={4}
|
||||||
<h3 className="text-xl font-black mb-1 italic tracking-tighter">STUDIO PRO</h3>
|
value={prompt}
|
||||||
<p className="text-sm font-bold mb-4 uppercase tracking-tighter opacity-80">Libere gerações ilimitadas e exportação em 48kHz.</p>
|
onChange={(e) => setPrompt(e.target.value)}
|
||||||
<BaseButton label="Fazer Upgrade" color="white" className="w-full font-black rounded-xl shadow-lg" />
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CardBox>
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<label className="text-[10px] font-black text-gray-500 uppercase px-2">Estilo de Música</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="w-full bg-[#1e1e1e] border-none text-sm text-white rounded-xl p-4 focus:ring-1 focus:ring-[#00E5FF] outline-none"
|
||||||
|
placeholder="Ex: Pop, Rock, Electronic, Hip-Hop"
|
||||||
|
value={style}
|
||||||
|
onChange={(e) => setStyle(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<label className="text-[10px] font-black text-gray-500 uppercase px-2">Cantor(a) IA</label>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setVoiceType('female')}
|
||||||
|
className={`p-2 rounded-xl text-xs font-bold border transition-all ${voiceType === 'female' ? 'bg-[#00E5FF] text-black border-[#00E5FF]' : 'bg-[#1e1e1e] border-white/5 text-gray-400'}`}
|
||||||
|
>
|
||||||
|
Voz Feminina
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setVoiceType('male')}
|
||||||
|
className={`p-2 rounded-xl text-xs font-bold border transition-all ${voiceType === 'male' ? 'bg-[#00E5FF] text-black border-[#00E5FF]' : 'bg-[#1e1e1e] border-white/5 text-gray-400'}`}
|
||||||
|
>
|
||||||
|
Voz Masculina
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isCustom && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<label className="text-[10px] font-black text-gray-500 uppercase px-2">Título (Opcional)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="w-full bg-[#1e1e1e] border-none text-sm text-white rounded-xl p-4 focus:ring-1 focus:ring-[#00E5FF] outline-none"
|
||||||
|
placeholder="Dê um nome ao seu hit"
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
disabled={generating}
|
||||||
|
onClick={handleCreate}
|
||||||
|
className="w-full bg-[#00E5FF] hover:bg-[#00B8CC] text-black font-black py-4 rounded-2xl transition-all shadow-[0_0_20px_rgba(0,229,255,0.2)] flex items-center justify-center disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{generating ? (
|
||||||
|
<span className="flex items-center"><BaseIcon path={mdiRefresh} spin className="mr-2" /> GERANDO...</span>
|
||||||
|
) : (
|
||||||
|
<span className="flex items-center"><BaseIcon path={mdiCreation} className="mr-2" /> CRIAR MÚSICA</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SectionMain>
|
|
||||||
<ToastContainer theme="dark" />
|
<div className="mt-auto pt-8">
|
||||||
</>
|
<div className="p-4 bg-gradient-to-br from-[#00E5FF]/10 to-[#BB86FC]/10 rounded-2xl border border-white/5">
|
||||||
|
<h4 className="text-xs font-black text-[#00E5FF] uppercase mb-1">Assinatura Grátis</h4>
|
||||||
|
<p className="text-[10px] text-gray-400 mb-3 uppercase tracking-tighter">10 Créditos restantes hoje</p>
|
||||||
|
<button className="w-full py-2 bg-white/5 hover:bg-white/10 text-white text-[10px] font-black rounded-lg transition-all uppercase tracking-widest">Fazer Upgrade</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{/* Main Content - Library Feed */}
|
||||||
|
<main className="flex-1 flex flex-col bg-[#0a0a0a] overflow-hidden">
|
||||||
|
{/* Top Header */}
|
||||||
|
<header className="h-16 border-b border-white/5 flex items-center justify-between px-8 bg-[#0a0a0a]/80 backdrop-blur-xl sticky top-0 z-20">
|
||||||
|
<div className="flex items-center space-x-6">
|
||||||
|
<button className="text-white font-black text-sm uppercase tracking-widest border-b-2 border-[#00E5FF] py-5">Minhas Criações</button>
|
||||||
|
<button className="text-gray-500 hover:text-white font-black text-sm uppercase tracking-widest py-5 transition-colors">Explorar</button>
|
||||||
|
<button className="text-gray-500 hover:text-white font-black text-sm uppercase tracking-widest py-5 transition-colors">Playlists</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<BaseIcon path={mdiAccountCircle} className="text-gray-400 hover:text-white cursor-pointer" />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Scrollable Feed */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-8 aside-scrollbars">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||||
|
{library.length > 0 ? library.map((track) => (
|
||||||
|
<div
|
||||||
|
key={track.id}
|
||||||
|
className={`group relative bg-[#121212] rounded-2xl overflow-hidden border border-white/5 hover:border-white/20 transition-all shadow-lg hover:-translate-y-1 ${currentTrack?.id === track.id ? 'ring-2 ring-[#00E5FF]' : ''}`}
|
||||||
|
>
|
||||||
|
{/* Thumbnail */}
|
||||||
|
<div className="aspect-square bg-gradient-to-br from-indigo-900 to-black relative">
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity bg-black/60 backdrop-blur-sm">
|
||||||
|
<button
|
||||||
|
onClick={() => playTrack(track)}
|
||||||
|
className="w-16 h-16 rounded-full bg-[#00E5FF] flex items-center justify-center text-black hover:scale-110 transition-transform shadow-2xl"
|
||||||
|
>
|
||||||
|
<BaseIcon path={(currentTrack?.id === track.id && isPlaying) ? mdiPause : mdiPlay} size={40} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="absolute bottom-2 right-2 px-2 py-0.5 bg-black/80 rounded text-[10px] font-black text-white uppercase tracking-tighter">
|
||||||
|
{track.bpm} BPM
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Info */}
|
||||||
|
<div className="p-4">
|
||||||
|
<h3 className="font-black text-white text-sm uppercase tracking-tighter truncate">{track.title}</h3>
|
||||||
|
<p className="text-gray-500 text-[10px] uppercase font-bold tracking-widest mb-3 truncate italic">{track.key_signature || 'AI Generated'}</p>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<BaseIcon path={mdiHeart} size={16} className="text-gray-600 hover:text-red-500 cursor-pointer transition-colors" />
|
||||||
|
<BaseIcon path={mdiShare} size={16} className="text-gray-600 hover:text-white cursor-pointer transition-colors" />
|
||||||
|
</div>
|
||||||
|
<div className="text-[8px] font-black text-gray-700 uppercase">AI VOCAL {track.ai_data?.voiceType || 'PRO'}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)) : (
|
||||||
|
<div className="col-span-full py-20 text-center">
|
||||||
|
<div className="w-20 h-20 bg-white/5 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<BaseIcon path={mdiPlaylistMusic} size={40} className="text-gray-700" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-black text-gray-300">Nenhuma música ainda</h3>
|
||||||
|
<p className="text-gray-600 uppercase text-xs font-bold tracking-widest mt-2">Comece a criar sua primeira música à esquerda!</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{/* Right Lyrics Sidebar (Optional) */}
|
||||||
|
{showLyrics && currentTrack && (
|
||||||
|
<aside className="w-80 bg-[#0a0a0a] border-l border-white/5 flex flex-col p-6 animate-fade-in overflow-y-auto aside-scrollbars">
|
||||||
|
<div className="flex items-center justify-between mb-8">
|
||||||
|
<h2 className="text-sm font-black text-white uppercase tracking-widest">LETRAS IA</h2>
|
||||||
|
<button onClick={() => setShowLyrics(false)} className="text-gray-500 hover:text-white">X</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h4 className="text-[10px] font-black text-[#00E5FF] uppercase mb-4 tracking-[0.2em]">[VERSO 1]</h4>
|
||||||
|
<p className="text-sm leading-relaxed text-gray-400 font-medium">
|
||||||
|
{currentTrack.ai_data?.lyrics?.verse1 || 'Gerando as letras...'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-[10px] font-black text-[#BB86FC] uppercase mb-4 tracking-[0.2em]">[REFRÃO]</h4>
|
||||||
|
<p className="text-sm leading-relaxed text-white font-bold">
|
||||||
|
{currentTrack.ai_data?.lyrics?.chorus || 'Gerando o refrão...'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-[10px] font-black text-[#00E5FF] uppercase mb-4 tracking-[0.2em]">[VERSO 2]</h4>
|
||||||
|
<p className="text-sm leading-relaxed text-gray-400 font-medium">
|
||||||
|
{currentTrack.ai_data?.lyrics?.verse2 || 'Continuando a história...'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Bottom Player */}
|
||||||
|
{currentTrack && (
|
||||||
|
<div className="fixed bottom-0 left-0 w-full h-24 bg-black/90 backdrop-blur-2xl border-t border-white/5 flex items-center px-6 z-50">
|
||||||
|
{/* Track Info */}
|
||||||
|
<div className="w-1/4 flex items-center space-x-4">
|
||||||
|
<div className="w-14 h-14 bg-gradient-to-br from-[#00E5FF] to-[#BB86FC] rounded-lg shrink-0 overflow-hidden">
|
||||||
|
<div className="w-full h-full flex items-center justify-center bg-black/20">
|
||||||
|
<BaseIcon path={mdiMusic} className="text-white" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<h4 className="text-white font-black text-sm uppercase italic truncate tracking-tighter">{currentTrack.title}</h4>
|
||||||
|
<p className="text-gray-500 text-[10px] font-bold uppercase tracking-widest truncate">{currentTrack.genre?.name || style}</p>
|
||||||
|
</div>
|
||||||
|
<BaseIcon path={mdiHeart} size={20} className="text-gray-600 hover:text-red-500 cursor-pointer ml-4 transition-colors" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Controls */}
|
||||||
|
<div className="flex-1 flex flex-col items-center space-y-2">
|
||||||
|
<div className="flex items-center space-x-6">
|
||||||
|
<button className="text-gray-500 hover:text-white transition-colors">
|
||||||
|
<BaseIcon path={mdiRefresh} size={20} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={togglePlayback}
|
||||||
|
className="w-10 h-10 rounded-full bg-white flex items-center justify-center text-black hover:scale-110 transition-transform"
|
||||||
|
>
|
||||||
|
<BaseIcon path={isPlaying ? mdiPause : mdiPlay} size={24} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowLyrics(!showLyrics)}
|
||||||
|
className={`transition-colors ${showLyrics ? 'text-[#00E5FF]' : 'text-gray-500 hover:text-white'}`}
|
||||||
|
>
|
||||||
|
<BaseIcon path={mdiPlus} size={20} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full max-w-xl flex items-center space-x-3">
|
||||||
|
<span className="text-[8px] font-black text-gray-600">0:00</span>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
value={progress}
|
||||||
|
onChange={handleProgressChange}
|
||||||
|
className="flex-1 h-1 bg-gray-800 rounded-full appearance-none cursor-pointer accent-[#00E5FF]"
|
||||||
|
/>
|
||||||
|
<span className="text-[8px] font-black text-gray-600">3:42</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Volume & More */}
|
||||||
|
<div className="w-1/4 flex items-center justify-end space-x-6">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<BaseIcon path={mdiVolumeHigh} size={20} className="text-gray-500" />
|
||||||
|
<div className="w-24 h-1 bg-gray-800 rounded-full overflow-hidden">
|
||||||
|
<div className="h-full bg-gray-400 w-3/4"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button className="text-gray-500 hover:text-white transition-colors">
|
||||||
|
<BaseIcon path={mdiDownload} size={20} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<audio
|
||||||
|
ref={audioRef}
|
||||||
|
onTimeUpdate={handleTimeUpdate}
|
||||||
|
onEnded={() => setIsPlaying(false)}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ToastContainer theme="dark" position="bottom-right" />
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
Studio.getLayout = function getLayout(page: ReactElement) {
|
SunoStudio.getLayout = function getLayout(page: ReactElement) {
|
||||||
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
|
return page; // No standard layout, using custom Suno-style
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Studio;
|
export default SunoStudio;
|
||||||
Loading…
x
Reference in New Issue
Block a user