diff --git a/backend/src/routes/locales.js b/backend/src/routes/locales.js index 5147309..e3a290d 100644 --- a/backend/src/routes/locales.js +++ b/backend/src/routes/locales.js @@ -24,34 +24,30 @@ router.get('/:lng/:ns.json', wrapAsync(async (req, res) => { } // 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 // 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. Only return the translated JSON object, nothing else. - JSON: ${JSON.stringify(enContent, null, 2)}`; + JSON: ${JSON.stringify(enContent)}`; try { const aiResponse = await LocalAIApi.createResponse({ 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 } ] }); 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 { - const translatedJson = JSON.parse(translatedText); + const translatedJson = LocalAIApi.decodeJsonFromResponse(aiResponse); // Optionally save the translated file to cache it for future use 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); } catch (parseError) { - console.error('Failed to parse AI translation JSON:', parseError, translatedText); - return res.status(500).json({ error: 'Invalid AI response format', details: translatedText }); + console.error('Failed to parse AI translation JSON:', parseError); + return res.status(500).json({ error: 'Invalid AI response format', details: parseError.message }); } } else { 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; \ No newline at end of file diff --git a/backend/src/services/ai_song_requests.js b/backend/src/services/ai_song_requests.js index 97b3300..71cf315 100644 --- a/backend/src/services/ai_song_requests.js +++ b/backend/src/services/ai_song_requests.js @@ -12,22 +12,48 @@ module.exports = class Ai_song_requestsService { static async create(data, currentUser) { const transaction = await db.sequelize.transaction(); 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 - const prompt = `Create a song structure 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", - "lyrics": {"verse1": "...", "chorus": "...", "verse2": "..."}`; + let prompt = ''; + if (isCustom) { + prompt = `Based on these lyrics: "${data.lyrics}", and style: "${data.style || 'Pop'}". + Create a full song configuration. + Return ONLY a JSON object with: + "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({ 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 } ] }); - 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) { try { const decoded = LocalAIApi.decodeJsonFromResponse(aiResponse); @@ -40,43 +66,40 @@ module.exports = class Ai_song_requestsService { // 2. Create the Project const project = await db.projects.create({ title: aiData.title, - status: 'in_progress', + status: 'completed', bpm: aiData.bpm, key_signature: aiData.key, ownerId: currentUser.id, createdBy: currentUser.id }, { 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({ - name: 'Main Beat', + name: 'Full Mix (Vocals + Music)', track_type: 'audio', projectId: project.id, order_index: 0, - volume: 0.8, + volume: 1.0, createdBy: currentUser.id }, { transaction }); - // 4. Create an Audio Clip (The "Real Beat") - // Using a royalty-free beat placeholder URL - // In a real scenario, this would be a file generated or fetched from a music AI service - const beatUrl = this.getBeatUrlByStyle(data.style || 'Pop'); + // 4. Get simulated AI Vocal URL + const vocalUrl = this.getSimulatedMusicUrl(data.style || 'Pop', voiceType); const audioClip = await db.audio_clips.create({ - name: 'Drum Beat', + name: 'AI Generated Song', trackId: track.id, start_bar: 0, - length_bars: 16, + length_bars: 32, gain: 1.0, createdBy: currentUser.id }, { transaction }); - // We should ideally create a File entry for this URL, but for the prototype - // we'll handle the URL in the frontend or add it to a specific field if available. - // Since 'audio_file' is a scope-based relation, we'll need to handle it properly. - - // Update the AI song request with the project ID - await Ai_song_requestsDBApi.create( + // Save lyrics to project or a separate field if we had it. + // For now, let's put them in the AI request details. + + // Update the AI song request with the project ID and enhanced data + const aiRequest = await Ai_song_requestsDBApi.create( { ...data, title: aiData.title, @@ -84,7 +107,9 @@ module.exports = class Ai_song_requestsService { projectId: project.id, target_bpm: aiData.bpm, 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, @@ -93,22 +118,47 @@ module.exports = class Ai_song_requestsService { ); 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) { if (transaction) await transaction.rollback(); throw error; } }; - static getBeatUrlByStyle(style) { - const beats = { - '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' + static getSimulatedMusicUrl(style, voiceType) { + // Mapping of styles and voices to high-quality placeholder tracks that include vocals + // These would be replaced by actual AI music generation API calls (like Suno/Udio/Replicate) + const samples = { + 'Pop': { + 'male': 'https://cdn.pixabay.com/audio/2022/10/14/audio_9939f04505.mp3', // Uplifting Pop + '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) { diff --git a/frontend/src/pages/studio.tsx b/frontend/src/pages/studio.tsx index ec08855..4a4939e 100644 --- a/frontend/src/pages/studio.tsx +++ b/frontend/src/pages/studio.tsx @@ -5,111 +5,109 @@ import { mdiPlay, mdiPause, mdiStop, - mdiPiano, mdiMicrophone, - mdiTuneVariant, mdiDownload, mdiRefresh, - mdiDelete + mdiDelete, + mdiMenu, + mdiChevronDown, + mdiCreation, + mdiPlaylistMusic, + mdiLibrary, + mdiAccountCircle, + mdiContentCopy, + mdiVolumeHigh, + mdiHeart, + mdiShare } from '@mdi/js'; import Head from 'next/head'; import React, { ReactElement, useEffect, useState, useRef } from 'react'; import CardBox from '../components/CardBox'; import LayoutAuthenticated from '../layouts/Authenticated'; import SectionMain from '../components/SectionMain'; -import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'; import { getPageTitle } from '../config'; import BaseIcon from '../components/BaseIcon'; import BaseButton from '../components/BaseButton'; -import FormField from '../components/FormField'; -import BaseDivider from '../components/BaseDivider'; import axios from 'axios'; -import { useAppDispatch, useAppSelector } from '../stores/hooks'; +import { useAppSelector } from '../stores/hooks'; import { toast, ToastContainer } from 'react-toastify'; -const Studio = () => { +const SunoStudio = () => { const { currentUser } = useAppSelector((state) => state.auth); - // State + // Create State + const [isCustom, setIsCustom] = useState(false); + const [lyrics, setLyrics] = useState(''); const [prompt, setPrompt] = useState(''); const [style, setStyle] = useState('Pop'); - const [projects, setProjects] = useState([]); - const [instruments, setInstruments] = useState([]); + const [title, setTitle] = useState(''); + const [voiceType, setVoiceType] = useState('female'); + + // Library State + const [library, setLibrary] = useState([]); const [loading, setLoading] = useState(false); const [generating, setGenerating] = useState(false); - const [currentProject, setCurrentProject] = useState(null); - // Audio Playback State + // Player State + const [currentTrack, setCurrentTrack] = useState(null); const [isPlaying, setIsPlaying] = useState(false); - const [audioUrl, setAudioUrl] = useState(''); const audioRef = useRef(null); - - // Recording State - const [isRecording, setIsRecording] = useState(false); - const [recordedBlob, setRecordedBlob] = useState(null); - const [recordedUrl, setRecordedUrl] = useState(''); - const mediaRecorderRef = useRef(null); - const audioChunksRef = useRef([]); + const [progress, setProgress] = useState(0); + const [showLyrics, setShowLyrics] = useState(false); useEffect(() => { - fetchStudioData(); + fetchLibrary(); }, []); - const fetchStudioData = async () => { + const fetchLibrary = async () => { try { setLoading(true); - const [projRes, instRes] = await Promise.all([ - axios.get('/projects?limit=5'), - axios.get('/instruments?limit=8') - ]); - setProjects(projRes.data.rows || []); - setInstruments(instRes.data.rows || []); + const response = await axios.get('/projects?limit=20'); + // We'll simulate library by fetching projects + setLibrary(response.data.rows || []); } catch (error) { - console.error('Error fetching studio data:', error); + console.error('Error fetching library:', error); } finally { setLoading(false); } }; - const getBeatUrlByStyle = (style: string) => { - const beats: any = { - '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) { + const handleCreate = async () => { + if (!isCustom && !prompt) { toast.error('Descreva sua ideia para a música'); return; } + if (isCustom && !lyrics) { + toast.error('Insira a letra da música'); + return; + } try { setGenerating(true); const response = await axios.post('/ai_song_requests', { data: { prompt_text: prompt, + lyrics: lyrics, 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 - const projRes = await axios.get('/projects?limit=1'); - const newProject = projRes.data.rows[0]; - setCurrentProject(newProject); + // Update library + setLibrary([newProject, ...library]); - // Set audio URL for the beat - const beat = getBeatUrlByStyle(style); - setAudioUrl(beat); + // Auto-play the new track + playTrack(newProject); - fetchStudioData(); + // Reset form + setPrompt(''); + setLyrics(''); + setTitle(''); } catch (error) { console.error('Error generating song:', error); 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 = () => { if (!audioRef.current) return; - if (isPlaying) { audioRef.current.pause(); } else { @@ -129,352 +138,315 @@ const Studio = () => { setIsPlaying(!isPlaying); }; - const startRecording = async () => { - try { - const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); - const mediaRecorder = new MediaRecorder(stream); - 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 handleTimeUpdate = () => { + if (audioRef.current) { + const p = (audioRef.current.currentTime / audioRef.current.duration) * 100; + setProgress(p); } }; - const stopRecording = () => { - if (mediaRecorderRef.current && isRecording) { - mediaRecorderRef.current.stop(); - setIsRecording(false); - - // Stop beat playback - if (audioRef.current) { - audioRef.current.pause(); - setIsPlaying(false); - } - - toast.success('Gravação finalizada!'); + const handleProgressChange = (e: React.ChangeEvent) => { + if (audioRef.current) { + const newTime = (parseFloat(e.target.value) / 100) * audioRef.current.duration; + audioRef.current.currentTime = newTime; + setProgress(parseFloat(e.target.value)); } }; return ( - <> +
- {getPageTitle('Studio Musical')} + {getPageTitle('AI Music Studio - Studio')} - - -
- + + {/* Left Sidebar - Create Section */} +