diff --git a/backend/src/db/api/ai_song_requests.js b/backend/src/db/api/ai_song_requests.js index fa5c10f..63f4b7e 100644 --- a/backend/src/db/api/ai_song_requests.js +++ b/backend/src/db/api/ai_song_requests.js @@ -89,7 +89,6 @@ module.exports = class Ai_song_requestsDBApi { return ai_song_requests; } - static async bulkImport(data, options) { const currentUser = (options && options.currentUser) || { id: null }; @@ -238,6 +237,10 @@ module.exports = class Ai_song_requestsDBApi { const currentUser = (options && options.currentUser) || { id: null }; const transaction = (options && options.transaction) || undefined; + if (!ids || !Array.isArray(ids) || ids.length === 0) { + return []; + } + const ai_song_requests = await db.ai_song_requests.findAll({ where: { id: { @@ -342,7 +345,7 @@ module.exports = class Ai_song_requestsDBApi { { model: db.users, as: 'user', - + required: !!filter.user, where: filter.user ? { [Op.or]: [ { id: { [Op.in]: filter.user.split('|').map(term => Utils.uuid(term)) } }, @@ -352,14 +355,14 @@ module.exports = class Ai_song_requestsDBApi { } }, ] - } : {}, + } : undefined, }, { model: db.genres, as: 'genre', - + required: !!filter.genre, where: filter.genre ? { [Op.or]: [ { id: { [Op.in]: filter.genre.split('|').map(term => Utils.uuid(term)) } }, @@ -369,14 +372,14 @@ module.exports = class Ai_song_requestsDBApi { } }, ] - } : {}, + } : undefined, }, { model: db.projects, as: 'project', - + required: !!filter.project, where: filter.project ? { [Op.or]: [ { id: { [Op.in]: filter.project.split('|').map(term => Utils.uuid(term)) } }, @@ -386,7 +389,7 @@ module.exports = class Ai_song_requestsDBApi { } }, ] - } : {}, + } : undefined, }, @@ -634,4 +637,4 @@ module.exports = class Ai_song_requestsDBApi { } -}; +}; \ No newline at end of file diff --git a/backend/src/db/api/projects.js b/backend/src/db/api/projects.js index e212f36..811191b 100644 --- a/backend/src/db/api/projects.js +++ b/backend/src/db/api/projects.js @@ -230,6 +230,10 @@ module.exports = class ProjectsDBApi { const currentUser = (options && options.currentUser) || { id: null }; const transaction = (options && options.transaction) || undefined; + if (!ids || !Array.isArray(ids) || ids.length === 0) { + return []; + } + const projects = await db.projects.findAll({ where: { id: { @@ -380,7 +384,7 @@ module.exports = class ProjectsDBApi { { model: db.users, as: 'owner', - + required: !!filter.owner, where: filter.owner ? { [Op.or]: [ { id: { [Op.in]: filter.owner.split('|').map(term => Utils.uuid(term)) } }, @@ -390,14 +394,14 @@ module.exports = class ProjectsDBApi { } }, ] - } : {}, + } : undefined, }, { model: db.genres, as: 'genre', - + required: !!filter.genre, where: filter.genre ? { [Op.or]: [ { id: { [Op.in]: filter.genre.split('|').map(term => Utils.uuid(term)) } }, @@ -407,7 +411,7 @@ module.exports = class ProjectsDBApi { } }, ] - } : {}, + } : undefined, }, diff --git a/backend/src/services/ai_song_requests.js b/backend/src/services/ai_song_requests.js index a7ab044..c3659e6 100644 --- a/backend/src/services/ai_song_requests.js +++ b/backend/src/services/ai_song_requests.js @@ -16,10 +16,11 @@ module.exports = class Ai_song_requestsService { const isCustom = data.is_custom === true || data.is_custom === 'true'; const voiceType = data.voice_type || 'female'; const style = data.style || 'Pop'; + const instrumental = data.instrumental === true || data.instrumental === 'true'; let prompt = ''; if (isCustom) { - prompt = `Based on these lyrics: "${data.lyrics}", and style: "${style}". + prompt = `Based on these lyrics: "${data.lyrics}", and style: "${style}". ${instrumental ? 'This should be an instrumental track.' : ''} Create a full professional song configuration. Return ONLY a JSON object with: "title": "${data.title || 'a creative song title'}", @@ -40,7 +41,7 @@ module.exports = class Ai_song_requestsService { "outro": "..." }`; } else { - prompt = `Create a full song based on this idea: "${data.prompt_text}". Style: ${style}. + prompt = `Create a full song based on this idea: "${data.prompt_text}". Style: ${style}. ${instrumental ? 'This should be an instrumental track.' : ''} Return ONLY a JSON object with: "title": "a creative song title", "bpm": a number between 60-180, @@ -74,13 +75,21 @@ module.exports = class Ai_song_requestsService { key: 'C Major', description: 'Professional AI Production', tags: ['Global', 'AI', style], - lyrics: { verse1: '...', chorus: '...' } + lyrics: { verse1: '...', chorus: '...' }, + original_request: { + style, + voiceType, + isCustom, + instrumental, + prompt_text: data.prompt_text, + lyrics: data.lyrics + } }; if (aiResponse.success) { try { const decoded = LocalAIApi.decodeJsonFromResponse(aiResponse); - if (decoded) aiData = { ...aiData, ...decoded }; + if (decoded) aiData = { ...aiData, ...decoded, original_request: aiData.original_request }; } catch (e) { console.error("Failed to decode AI response", e); } diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts index 7a0a5f6..974f899 100644 --- a/frontend/src/i18n.ts +++ b/frontend/src/i18n.ts @@ -8,13 +8,13 @@ const countryLanguageDetector = { name: 'countryLanguageDetector', lookup() { if (typeof window !== 'undefined' && window.localStorage) { - return localStorage.getItem('detected_country_lang'); + return window.localStorage.getItem('detected_country_lang'); } return undefined; }, cacheUserLanguage(lng: string) { if (typeof window !== 'undefined' && window.localStorage) { - localStorage.setItem('detected_country_lang', lng); + window.localStorage.setItem('detected_country_lang', lng); } } }; @@ -44,7 +44,7 @@ i18n }); // Perform country detection asynchronously if not already detected -if (typeof window !== 'undefined' && window.localStorage && !localStorage.getItem('detected_country_lang')) { +if (typeof window !== 'undefined' && window.localStorage && !window.localStorage.getItem('detected_country_lang')) { fetch('https://ipapi.co/json/') .then(res => res.json()) .then(data => { @@ -53,9 +53,11 @@ if (typeof window !== 'undefined' && window.localStorage && !localStorage.getIte const languages = data.languages.split(','); if (languages.length > 0) { const baseLang = languages[0].split('-')[0]; - localStorage.setItem('detected_country_lang', baseLang); - if (!localStorage.getItem('app_lang_')) { - i18n.changeLanguage(baseLang); + if (typeof window !== 'undefined' && window.localStorage) { + window.localStorage.setItem('detected_country_lang', baseLang); + if (!window.localStorage.getItem('app_lang_')) { + i18n.changeLanguage(baseLang); + } } } } diff --git a/frontend/src/pages/_app.tsx b/frontend/src/pages/_app.tsx index 71881be..7e7fdd6 100644 --- a/frontend/src/pages/_app.tsx +++ b/frontend/src/pages/_app.tsx @@ -13,7 +13,7 @@ import ErrorBoundary from "../components/ErrorBoundary"; import DevModeBadge from '../components/DevModeBadge'; import 'intro.js/introjs.css'; import { appWithTranslation } from 'next-i18next'; -import '../i18n'; +// import '../i18n'; import IntroGuide from '../components/IntroGuide'; import { appSteps, loginSteps, usersSteps, rolesSteps } from '../stores/introSteps'; @@ -40,27 +40,33 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) { const [stepName, setStepName] = React.useState(''); const [steps, setSteps] = React.useState([]); - axios.interceptors.request.use( - config => { - const token = localStorage.getItem('token'); + React.useEffect(() => { + const requestInterceptor = axios.interceptors.request.use( + config => { + const token = typeof window !== 'undefined' ? localStorage.getItem('token') : null; - if (token) { - config.headers.Authorization = `Bearer ${token}`; - } else { - delete config.headers.Authorization; + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } else { + delete config.headers.Authorization; + } + + return config; + }, + error => { + return Promise.reject(error); } + ); - return config; - }, - error => { - return Promise.reject(error); - } - ); + return () => { + axios.interceptors.request.eject(requestInterceptor); + }; + }, []); // TODO: Remove this code in future releases React.useEffect(() => { const allowedOrigin = (() => { - if (!document.referrer) { + if (typeof window === 'undefined' || !document.referrer) { return null; } try { @@ -115,7 +121,7 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) { // Tour is disabled by default in generated projects. return; const isCompleted = (stepKey: string) => { - return localStorage.getItem(`completed_${stepKey}`) === 'true'; + return typeof window !== 'undefined' ? localStorage.getItem(`completed_${stepKey}`) === 'true' : false; }; if (router.pathname === '/login' && !isCompleted('loginSteps')) { setSteps(loginSteps); diff --git a/frontend/src/pages/studio.tsx b/frontend/src/pages/studio.tsx index 6a747d7..b2c2254 100644 --- a/frontend/src/pages/studio.tsx +++ b/frontend/src/pages/studio.tsx @@ -24,52 +24,45 @@ import { mdiZipBox, mdiTune, mdiInstrumentTriangle, - mdiSparkles + mdiEarth, + mdiDotsVertical, + mdiSkipNext, + mdiSkipPrevious, + mdiVolumeMedium, + mdiVolumeMute } 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 { getPageTitle } from '../config'; import BaseIcon from '../components/BaseIcon'; -import BaseButton from '../components/BaseButton'; import axios from 'axios'; import { useAppSelector } from '../stores/hooks'; import { toast, ToastContainer } from 'react-toastify'; - -const GENRES = [ - 'Pop', 'Rock', 'Hip-Hop', 'Trap', 'Electronic', 'Jazz', 'Reggae', - 'Classical', 'Bossa Nova', 'Metal', 'Lo-Fi', 'Reggaeton', 'Country', - 'Synthwave', 'Phonk', 'K-Pop', 'R&B', 'Disco', 'Funk', 'Soul', 'Gospel' -]; +import 'react-toastify/dist/ReactToastify.css'; const SunoStudio = () => { const { currentUser } = useAppSelector((state) => state.auth); - - // Create State + const [activeTab, setActiveTab] = useState<'create' | 'library' | 'explore'>('create'); const [isCustom, setIsCustom] = useState(false); + const [instrumental, setInstrumental] = useState(false); const [lyrics, setLyrics] = useState(''); const [prompt, setPrompt] = useState(''); const [style, setStyle] = useState('Pop'); const [title, setTitle] = useState(''); const [voiceType, setVoiceType] = useState('female'); - const [isPromptRefining, setIsPromptRefining] = useState(false); - // Library State const [library, setLibrary] = useState([]); const [loading, setLoading] = useState(false); - const [generating, setGenerating] = useState(false); - const [generationStep, setGenerationStep] = useState(0); + const [isGenerating, setIsGenerating] = useState(false); - // Player State const [currentTrack, setCurrentTrack] = useState(null); const [isPlaying, setIsPlaying] = useState(false); const audioRef = useRef(null); const [progress, setProgress] = useState(0); - const [duration, setDuration] = useState(0); const [currentTime, setCurrentTime] = useState(0); - const [showLyrics, setShowLyrics] = useState(false); + const [duration, setDuration] = useState(0); + const [volume, setVolume] = useState(0.8); useEffect(() => { fetchLibrary(); @@ -87,68 +80,49 @@ const SunoStudio = () => { } }; - 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; - } - + const handleGenerate = async () => { + if (isGenerating) return; + try { - setGenerating(true); - - setGenerationStep(1); // Writing Lyrics & Composition - await new Promise(r => setTimeout(r, 3000)); - - setGenerationStep(2); // Arrangement & Instruments - await new Promise(r => setTimeout(r, 3500)); - - setGenerationStep(3); // Vocal Synthesis & Tuning - await new Promise(r => setTimeout(r, 4000)); - - setGenerationStep(4); // Final Master & Rendering - - const response = await axios.post('/ai_song_requests', { + setIsGenerating(true); + const payload = { data: { - prompt_text: prompt, - lyrics: lyrics, - style: style, - title: title || `AI Project - ${style}`, + title: title || 'New AI Hit', + prompt_text: isCustom ? lyrics : prompt, + lyrics: isCustom ? lyrics : '', + style, is_custom: isCustom, - voice_type: voiceType + voice_type: voiceType, + instrumental } - }); + }; - const newProject = response.data; - toast.success('HIT MUNDIAL GERADO COM SUCESSO!'); + const response = await axios.post('/ai_song_requests', payload); - fetchLibrary(); - playTrack(newProject); - - // Reset form - setPrompt(''); - setLyrics(''); - setTitle(''); + if (response.status === 200) { + toast.success('Sua música está sendo gerada!', { theme: 'dark' }); + fetchLibrary(); + setActiveTab('library'); + } } catch (error) { console.error('Error generating song:', error); - toast.error('Erro no motor de geração. Verifique sua conexão.'); + toast.error('Erro ao gerar música. Tente novamente.', { theme: 'dark' }); } finally { - setGenerating(false); - setGenerationStep(0); + setIsGenerating(false); } }; const playTrack = (track: any) => { + if (currentTrack?.id === track.id) { + togglePlayback(); + return; + } setCurrentTrack(track); setIsPlaying(true); - 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().catch(e => console.error("Playback failed", e)); + audioRef.current.src = track?.audio_url; + audioRef.current.load(); + audioRef.current.play().catch((e: any) => console.error('Playback error:', e)); } }; @@ -157,7 +131,7 @@ const SunoStudio = () => { if (isPlaying) { audioRef.current.pause(); } else { - audioRef.current.play(); + audioRef.current.play().catch((e: any) => console.error('Playback error:', e)); } setIsPlaying(!isPlaying); }; @@ -165,14 +139,17 @@ const SunoStudio = () => { const handleTimeUpdate = () => { if (audioRef.current) { setCurrentTime(audioRef.current.currentTime); - const p = (audioRef.current.currentTime / audioRef.current.duration) * 100; - setProgress(p || 0); + setDuration(audioRef.current.duration || 0); + setProgress((audioRef.current.currentTime / audioRef.current.duration) * 100 || 0); } }; - const handleLoadedMetadata = () => { + const handleSeek = (e: React.ChangeEvent) => { + const val = parseFloat(e.target.value); if (audioRef.current) { - setDuration(audioRef.current.duration); + const newTime = (val / 100) * (audioRef.current.duration || 0); + audioRef.current.currentTime = newTime; + setProgress(val); } }; @@ -183,522 +160,289 @@ const SunoStudio = () => { return `${mins}:${secs.toString().padStart(2, '0')}`; }; - 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)); - } - }; - - const handleDownload = (track: any = currentTrack) => { - if (!track) return; - const url = track.audio_url || 'https://cdn.pixabay.com/audio/2023/10/24/audio_333458421d.mp3'; - const link = document.createElement('a'); - link.href = url; - link.setAttribute('download', `${track.title || 'AI_Music_Studio'}.mp3`); - link.setAttribute('target', '_blank'); - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - toast.info(`Baixando: ${track.title}`); - }; - - const handleDownloadAll = () => { - if (library.length === 0) return; - toast.info('Iniciando download de todas as criações...'); - library.forEach((track, index) => { - setTimeout(() => { - handleDownload(track); - }, index * 1000); // Small delay to avoid browser blocking multiple downloads - }); - }; - - const handleDelete = async (id: string) => { - if (confirm('Deseja excluir permanentemente este hit?')) { - try { - await axios.post('/projects/deleteByIds', { ids: [id] }); - setLibrary(library.filter(t => t.id !== id)); - if (currentTrack?.id === id) { - setCurrentTrack(null); - setIsPlaying(false); - } - toast.info('Hit removido da biblioteca'); - } catch (e) { - toast.error('Erro ao excluir'); + const deleteTrack = async (id: string) => { + if (!confirm('Deseja excluir esta música?')) return; + try { + await axios.delete(`/projects/${id}`); + fetchLibrary(); + if (currentTrack?.id === id) { + setCurrentTrack(null); + setIsPlaying(false); } + toast.info('Música removida', { theme: 'dark' }); + } catch (error) { + toast.error('Erro ao excluir', { theme: 'dark' }); } }; return ( -
+
- {getPageTitle('STUDIO PRO - Global AI Music Engine')} + {getPageTitle('STUDIO AI')} + - {/* Left Sidebar */} -