From 1fe82c25362cd64f978c73f637901139302dd87f Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Sun, 1 Mar 2026 21:23:57 +0000 Subject: [PATCH] 4 --- backend/src/services/ai_song_requests.js | 97 +++++- frontend/src/pages/studio.tsx | 424 +++++++++++++++++------ 2 files changed, 404 insertions(+), 117 deletions(-) diff --git a/backend/src/services/ai_song_requests.js b/backend/src/services/ai_song_requests.js index af347db..97b3300 100644 --- a/backend/src/services/ai_song_requests.js +++ b/backend/src/services/ai_song_requests.js @@ -6,17 +6,86 @@ const csv = require('csv-parser'); const axios = require('axios'); const config = require('../config'); const stream = require('stream'); - - - - +const { LocalAIApi } = require('../ai/LocalAIApi'); module.exports = class Ai_song_requestsService { static async create(data, currentUser) { const transaction = await db.sequelize.transaction(); try { + // 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": "..."}`; + + const aiResponse = await LocalAIApi.createResponse({ + input: [ + { role: 'system', content: 'You are a professional music producer. Return only valid JSON.' }, + { role: 'user', content: prompt } + ] + }); + + let aiData = { title: data.title || 'New AI Song', bpm: 120, key: 'C Major' }; + if (aiResponse.success) { + try { + const decoded = LocalAIApi.decodeJsonFromResponse(aiResponse); + if (decoded) aiData = { ...aiData, ...decoded }; + } catch (e) { + console.error("Failed to decode AI response", e); + } + } + + // 2. Create the Project + const project = await db.projects.create({ + title: aiData.title, + status: 'in_progress', + bpm: aiData.bpm, + key_signature: aiData.key, + ownerId: currentUser.id, + createdBy: currentUser.id + }, { transaction }); + + // 3. Create a Track for the beat + const track = await db.tracks.create({ + name: 'Main Beat', + track_type: 'audio', + projectId: project.id, + order_index: 0, + volume: 0.8, + 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'); + + const audioClip = await db.audio_clips.create({ + name: 'Drum Beat', + trackId: track.id, + start_bar: 0, + length_bars: 16, + 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( - data, + { + ...data, + title: aiData.title, + status: 'succeeded', + projectId: project.id, + target_bpm: aiData.bpm, + key_signature: aiData.key, + completed_at: new Date() + }, { currentUser, transaction, @@ -24,12 +93,24 @@ module.exports = class Ai_song_requestsService { ); await transaction.commit(); + return project; } catch (error) { - await transaction.rollback(); + 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' + }; + return beats[style] || beats['Pop']; + } + static async bulkImport(req, res, sendInvitationEmails = true, host) { const transaction = await db.sequelize.transaction(); @@ -133,6 +214,4 @@ module.exports = class Ai_song_requestsService { } -}; - - +}; \ No newline at end of file diff --git a/frontend/src/pages/studio.tsx b/frontend/src/pages/studio.tsx index 42ca860..ec08855 100644 --- a/frontend/src/pages/studio.tsx +++ b/frontend/src/pages/studio.tsx @@ -3,13 +3,17 @@ import { mdiRobotOutline, mdiPlus, mdiPlay, + mdiPause, + mdiStop, mdiPiano, - mdiGuitarAcoustic, mdiMicrophone, - mdiTuneVariant + mdiTuneVariant, + mdiDownload, + mdiRefresh, + mdiDelete } from '@mdi/js'; import Head from 'next/head'; -import React, { ReactElement, useEffect, useState } from 'react'; +import React, { ReactElement, useEffect, useState, useRef } from 'react'; import CardBox from '../components/CardBox'; import LayoutAuthenticated from '../layouts/Authenticated'; import SectionMain from '../components/SectionMain'; @@ -21,19 +25,31 @@ import FormField from '../components/FormField'; import BaseDivider from '../components/BaseDivider'; import axios from 'axios'; import { useAppDispatch, useAppSelector } from '../stores/hooks'; -import { aiResponse } from '../stores/openAiSlice'; import { toast, ToastContainer } from 'react-toastify'; const Studio = () => { - const dispatch = useAppDispatch(); const { currentUser } = useAppSelector((state) => state.auth); - const { isAskingResponse, aiResponse: gptResult } = useAppSelector((state) => state.openAi); + // State const [prompt, setPrompt] = useState(''); const [style, setStyle] = useState('Pop'); const [projects, setProjects] = useState([]); const [instruments, setInstruments] = useState([]); const [loading, setLoading] = useState(false); + const [generating, setGenerating] = useState(false); + const [currentProject, setCurrentProject] = useState(null); + + // Audio Playback State + 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([]); useEffect(() => { fetchStudioData(); @@ -55,119 +71,309 @@ const Studio = () => { } }; + 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) { - toast.error('Please enter an idea for your song'); + toast.error('Descreva sua ideia para a música'); return; } - const fullPrompt = `Create a song structure based on this idea: "${prompt}". Style: ${style}. Return a JSON with title, lyrics (verses, chorus), and suggested instruments.`; - - dispatch(aiResponse({ - input: [ - { role: 'system', content: 'You are a professional music producer and songwriter.' }, - { role: 'user', content: fullPrompt }, - ], - options: { poll_interval: 5, poll_timeout: 300 }, - })); + try { + setGenerating(true); + const response = await axios.post('/ai_song_requests', { + data: { + prompt_text: prompt, + style: style, + title: `AI Project - ${style}` + } + }); + + toast.success('Música e batida geradas com sucesso!'); + + // Fetch latest projects to get the new one + const projRes = await axios.get('/projects?limit=1'); + const newProject = projRes.data.rows[0]; + setCurrentProject(newProject); + + // Set audio URL for the beat + const beat = getBeatUrlByStyle(style); + setAudioUrl(beat); + + fetchStudioData(); + } catch (error) { + console.error('Error generating song:', error); + toast.error('Erro ao gerar música. Tente novamente.'); + } finally { + setGenerating(false); + } }; - useEffect(() => { - if (gptResult) { - toast.success('AI has generated your song structure!'); - // In a real app, we would save this as a new project + const togglePlayback = () => { + if (!audioRef.current) return; + + if (isPlaying) { + audioRef.current.pause(); + } else { + audioRef.current.play(); } - }, [gptResult]); + 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 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!'); + } + }; return ( <> - {getPageTitle('Musical Studio')} + {getPageTitle('Studio Musical')} - - toast.info('Manual project creation coming soon!')} - /> + +
+ +
- {/* AI Generator Section */} + {/* Main Area */}
- -
- -
-
-

- - AI SONGWRITER -

-

Describe your idea, choose a style, and let AI build the foundation of your next hit.

- -
- -