38920-vm/backend/src/services/ai_song_requests.js
Flatlogic Bot 7c8a152858 5
2026-03-01 21:58:02 +00:00

267 lines
8.5 KiB
JavaScript

const db = require('../db/models');
const Ai_song_requestsDBApi = require('../db/api/ai_song_requests');
const processFile = require("../middlewares/upload");
const ValidationError = require('./notifications/errors/validation');
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 {
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
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 and songwriter. You excel at creating Suno-style song metadata. Return only valid JSON.' },
{ role: 'user', content: prompt }
]
});
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);
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: 'completed',
bpm: aiData.bpm,
key_signature: aiData.key,
ownerId: currentUser.id,
createdBy: currentUser.id
}, { transaction });
// 3. Create a Track for the AI Vocal + Beat (Merged like Suno)
const track = await db.tracks.create({
name: 'Full Mix (Vocals + Music)',
track_type: 'audio',
projectId: project.id,
order_index: 0,
volume: 1.0,
createdBy: currentUser.id
}, { transaction });
// 4. Get simulated AI Vocal URL
const vocalUrl = this.getSimulatedMusicUrl(data.style || 'Pop', voiceType);
const audioClip = await db.audio_clips.create({
name: 'AI Generated Song',
trackId: track.id,
start_bar: 0,
length_bars: 32,
gain: 1.0,
createdBy: currentUser.id
}, { transaction });
// 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,
status: 'succeeded',
projectId: project.id,
target_bpm: aiData.bpm,
key_signature: aiData.key,
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,
transaction,
},
);
await transaction.commit();
// 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 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'
}
};
const styleSamples = samples[style] || samples['Pop'];
return styleSamples[voiceType] || styleSamples['both'];
}
static async bulkImport(req, res, sendInvitationEmails = true, host) {
const transaction = await db.sequelize.transaction();
try {
await processFile(req, res);
const bufferStream = new stream.PassThrough();
const results = [];
await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream
await new Promise((resolve, reject) => {
bufferStream
.pipe(csv())
.on('data', (data) => results.push(data))
.on('end', async () => {
console.log('CSV results', results);
resolve();
})
.on('error', (error) => reject(error));
})
await Ai_song_requestsDBApi.bulkImport(results, {
transaction,
ignoreDuplicates: true,
validate: true,
currentUser: req.currentUser
});
await transaction.commit();
} catch (error) {
await transaction.rollback();
throw error;
}
}
static async update(data, id, currentUser) {
const transaction = await db.sequelize.transaction();
try {
let ai_song_requests = await Ai_song_requestsDBApi.findBy(
{id},
{transaction},
);
if (!ai_song_requests) {
throw new ValidationError(
'ai_song_requestsNotFound',
);
}
const updatedAi_song_requests = await Ai_song_requestsDBApi.update(
id,
data,
{
currentUser,
transaction,
},
);
await transaction.commit();
return updatedAi_song_requests;
} catch (error) {
await transaction.rollback();
throw error;
}
};
static async deleteByIds(ids, currentUser) {
const transaction = await db.sequelize.transaction();
try {
await Ai_song_requestsDBApi.deleteByIds(ids, {
currentUser,
transaction,
});
await transaction.commit();
} catch (error) {
await transaction.rollback();
throw error;
}
}
static async remove(id, currentUser) {
const transaction = await db.sequelize.transaction();
try {
await Ai_song_requestsDBApi.remove(
id,
{
currentUser,
transaction,
},
);
await transaction.commit();
} catch (error) {
await transaction.rollback();
throw error;
}
}
};