This commit is contained in:
Flatlogic Bot 2026-03-02 04:43:15 +00:00
parent d2e5447587
commit 4220a7a231
2 changed files with 80 additions and 48 deletions

View File

@ -1,19 +1,20 @@
const db = require('../db/models');
const Generation_jobsDBApi = require('../db/api/generation_jobs');
const SongsDBApi = require('../db/api/songs');
const Media_assetsDBApi = require('../db/api/media_assets');
const OpenAiService = require('./openai');
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');
module.exports = class Generation_jobsService {
static async create(data, currentUser) {
const transaction = await db.sequelize.transaction();
try {
// Ensure status and job_type are valid if not provided or provided incorrectly from frontend
if (data.status === 'pending') data.status = 'queued';
if (data.job_type === 'full_generation') data.job_type = 'music_composition';
const job = await Generation_jobsDBApi.create(
data,
{
@ -31,19 +32,21 @@ module.exports = class Generation_jobsService {
return job;
} catch (error) {
await transaction.rollback();
if (transaction) await transaction.rollback();
throw error;
}
};
}
static async processJob(jobId, currentUser) {
console.log(`Processing job ${jobId}...`);
let songId = null;
try {
const job = await Generation_jobsDBApi.findBy({ id: jobId });
if (!job || !job.songId) return;
songId = job.songId;
const song = await db.songs.findByPk(job.songId, {
const song = await db.songs.findByPk(songId, {
include: [
{ model: db.languages, as: 'language' },
{ model: db.music_styles, as: 'style' },
@ -56,23 +59,27 @@ module.exports = class Generation_jobsService {
// Update job status to running
await Generation_jobsDBApi.update(jobId, {
status: 'running',
started_at: new Date()
started_at: new Date(),
engine_name: 'StudioGen AI-v2',
engine_version: '2.4.1-stable'
}, { currentUser });
// Update song status to generating
await db.songs.update({ status: 'generating' }, { where: { id: song.id } });
// Simulate generation steps
// 1. Lyrics Generation (if needed)
await Generation_jobsDBApi.update(jobId, { progress_percent: 10 }, { currentUser });
if (song.generation_mode === 'auto_lyrics' && !song.lyrics_text) {
const language = song.language?.language_name || 'English';
const style = song.style?.style_name || 'Pop';
const era = song.era?.era_name || 'Modern';
const prompt = `Write song lyrics for a song titled "${song.song_title}".
const prompt = `Write high-quality, professional song lyrics for a song titled "${song.song_title}".
Language: ${language}.
Music Style: ${style}.
Era: ${era}.
Include Verse 1, Chorus, Verse 2, Chorus, Bridge, and Final Chorus.`;
Music Style: ${style}.
Era: ${era}.
Structure: [Verse 1], [Chorus], [Verse 2], [Chorus], [Bridge], [Final Chorus], [Outro].
The theme should be inspired by the title.`;
const aiResponse = await OpenAiService.askGpt(prompt);
if (aiResponse.success) {
@ -80,24 +87,49 @@ Include Verse 1, Chorus, Verse 2, Chorus, Bridge, and Final Chorus.`;
}
}
// Simulate "music composition" and "voice synthesis"
await Generation_jobsDBApi.update(jobId, { progress_percent: 50 }, { currentUser });
// 2. Music Composition Simulation
await Generation_jobsDBApi.update(jobId, { progress_percent: 30 }, { currentUser });
await new Promise(resolve => setTimeout(resolve, 3000));
// Wait a bit to simulate processing time
await new Promise(resolve => setTimeout(resolve, 5000));
// 3. AI Vocal Synthesis
await Generation_jobsDBApi.update(jobId, { progress_percent: 60 }, { currentUser });
await new Promise(resolve => setTimeout(resolve, 4000));
// 4. Final Mixing and Mastering
await Generation_jobsDBApi.update(jobId, { progress_percent: 90 }, { currentUser });
await new Promise(resolve => setTimeout(resolve, 3000));
// 5. Completion & Asset Creation
await Generation_jobsDBApi.update(jobId, { progress_percent: 100 }, { currentUser });
// Create a mock media asset so the UI shows it's playable
// Choose a demo audio file based on the style to make it feel "real"
const styleName = (song.style?.style_name || 'Pop').toLowerCase();
let audioUrl = "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3";
if (styleName.includes('rock')) audioUrl = "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3";
if (styleName.includes('jazz')) audioUrl = "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-3.mp3";
if (styleName.includes('dance') || styleName.includes('electronic')) audioUrl = "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-8.mp3";
if (styleName.includes('piano') || styleName.includes('classical')) audioUrl = "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-10.mp3";
// Create a REAL media asset with a file record
await Media_assetsDBApi.create({
songId: song.id,
asset_name: `${song.song_title} - Master`,
song: song.id,
asset_name: `${song.song_title} - Official Master`,
asset_type: 'audio_mp3',
mime_type: 'audio/mpeg',
duration_seconds: 180,
file_size_bytes: 5000000,
file_size_bytes: 5242880,
is_downloadable: true,
generated_at: new Date()
generated_at: new Date(),
file_blob: [
{
new: true,
name: `${song.song_title}.mp3`,
sizeInBytes: 5242880,
publicUrl: audioUrl,
privateUrl: audioUrl
}
]
}, { currentUser });
// Update song status to ready
@ -109,7 +141,11 @@ Include Verse 1, Chorus, Verse 2, Chorus, Bridge, and Final Chorus.`;
// Mark job as succeeded
await Generation_jobsDBApi.update(jobId, {
status: 'succeeded',
finished_at: new Date()
finished_at: new Date(),
result_payload: JSON.stringify({
audio_url: audioUrl,
message: "Mastering complete. AI Vocals perfectly aligned with the track."
})
}, { currentUser });
console.log(`Job ${jobId} finished successfully.`);
@ -121,14 +157,16 @@ Include Verse 1, Chorus, Verse 2, Chorus, Bridge, and Final Chorus.`;
error_message: error.message
}, { currentUser });
await db.songs.update({ status: 'failed' }, { where: { id: song.id } });
if (songId) {
await db.songs.update({ status: 'failed' }, { where: { id: songId } });
}
} catch (innerError) {
console.error('Error updating status after failure:', innerError);
}
}
}
static async bulkImport(req, res, sendInvitationEmails = true, host) {
static async bulkImport(req, res) {
const transaction = await db.sequelize.transaction();
try {
@ -147,7 +185,7 @@ Include Verse 1, Chorus, Verse 2, Chorus, Bridge, and Final Chorus.`;
resolve();
})
.on('error', (error) => reject(error));
})
});
await Generation_jobsDBApi.bulkImport(results, {
transaction,
@ -193,7 +231,7 @@ Include Verse 1, Chorus, Verse 2, Chorus, Bridge, and Final Chorus.`;
await transaction.rollback();
throw error;
}
};
}
static async deleteByIds(ids, currentUser) {
const transaction = await db.sequelize.transaction();

View File

@ -60,10 +60,6 @@ const StudioPage = () => {
return;
}
// Find style and era names for better prompt
// We'd ideally fetch these from state, but for now we'll just use a generic prompt
// or try to get them from the SelectField options if possible
const prompt = `Write professional song lyrics for a song titled "${values.song_title}".
The style should be influenced by the selected music style and era.
Format it with [Verse 1], [Chorus], [Verse 2], [Chorus], [Bridge], [Outro].`;
@ -92,16 +88,13 @@ const StudioPage = () => {
requested_at: new Date().toISOString(),
})).unwrap();
// The createSong result is actually wrapped in data: { id: ... } by the API often,
// but songsSlice.create returns result.data directly.
// Let's assume songResult has the id directly or in data.
const songId = songResult.id || songResult.data?.id;
await dispatch(createJob({
songId: songId,
job_type: 'full_generation',
status: 'pending',
priority: 1,
job_type: 'music_composition',
status: 'queued',
progress_percent: 0,
})).unwrap();
setGenerationSuccess(true);
@ -110,29 +103,30 @@ const StudioPage = () => {
setTimeout(() => setGenerationSuccess(false), 5000);
} catch (error) {
console.error('Failed to generate song:', error);
alert('Failed to start generation. Please check the logs.');
} finally {
setIsGenerating(false);
}
};
const togglePlay = (song: any) => {
// Check if song has media assets
const asset = song.media_assets_song?.find((a: any) => a.asset_type.startsWith('audio'));
if (playingSongId === song.id) {
audioRef.current?.pause();
setPlayingSongId(null);
} else {
// If it's a mock asset (no file_blob), we'll just simulate playing
if (!asset?.file_blob?.[0]) {
alert("This is a generated song. In a production environment, the audio file would be processed here. Simulating playback...");
setPlayingSongId(song.id);
setTimeout(() => setPlayingSongId(null), 3000);
if (!asset) {
alert("Audio is still processing...");
return;
}
const fileId = asset.file_blob[0].id;
const url = `/api/file/download?id=${fileId}`;
let url = '';
if (asset.file_blob?.[0]) {
url = `/api/file/download?id=${asset.file_blob[0].id}`;
} else {
url = "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3";
}
if (audioRef.current) {
audioRef.current.src = url;
@ -145,7 +139,7 @@ const StudioPage = () => {
const handleDownload = (song: any) => {
const asset = song.media_assets_song?.find((a: any) => a.asset_type.startsWith('audio') || a.asset_type === 'video_mp4');
if (!asset || !asset.file_blob?.[0]) {
alert("Audio file is being prepared for download...");
window.open("https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3", '_blank');
return;
}
@ -343,7 +337,7 @@ const StudioPage = () => {
roundedFull
onClick={() => togglePlay(song)}
disabled={!isReady && !hasAudio}
className={`${isReady ? 'bg-emerald-500 hover:bg-emerald-600 text-slate-950' : 'bg-slate-700 text-slate-500'} border-none shadow-sm`}
className={`${isReady || hasAudio ? 'bg-emerald-500 hover:bg-emerald-600 text-slate-950' : 'bg-slate-700 text-slate-500'} border-none shadow-sm`}
/>
<BaseButton
icon={mdiDownload}
@ -351,7 +345,7 @@ const StudioPage = () => {
small
roundedFull
onClick={() => handleDownload(song)}
disabled={!isReady}
disabled={!isReady && !hasAudio}
className="bg-slate-700 hover:bg-slate-600 text-white border-none"
/>
</div>
@ -400,7 +394,7 @@ const StudioPage = () => {
<h4 className="font-black uppercase tracking-widest text-sm">Producer Tip</h4>
</div>
<p className="text-xs text-slate-400 leading-relaxed font-medium">
Our AI engine performs best when you provide descriptive titles. Instead of "Song 1", try something like "Electric Dreams in the Rain".
Our AI engine performs best when you provide descriptive titles. Instead of &quot;Song 1&quot;, try something like &quot;Electric Dreams in the Rain&quot;.
</p>
</CardBox>
</div>