This commit is contained in:
Flatlogic Bot 2026-03-02 04:21:28 +00:00
parent f1be1e14ea
commit d2e5447587
6 changed files with 265 additions and 100 deletions

View File

@ -1,4 +1,3 @@
const express = require('express');
const Generation_jobsService = require('../services/generation_jobs');
@ -97,8 +96,7 @@ router.use(checkCrudPermissions('generation_jobs'));
router.post('/', wrapAsync(async (req, res) => {
const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`;
const link = new URL(referer);
await Generation_jobsService.create(req.body.data, req.currentUser, true, link.host);
const payload = true;
const payload = await Generation_jobsService.create(req.body.data, req.currentUser, true, link.host);
res.status(200).send(payload);
}));
@ -446,4 +444,4 @@ router.get('/:id', wrapAsync(async (req, res) => {
router.use('/', require('../helpers').commonErrorHandler);
module.exports = router;
module.exports = router;

View File

@ -1,4 +1,3 @@
const express = require('express');
const SongsService = require('../services/songs');
@ -91,8 +90,7 @@ router.use(checkCrudPermissions('songs'));
router.post('/', wrapAsync(async (req, res) => {
const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`;
const link = new URL(referer);
await SongsService.create(req.body.data, req.currentUser, true, link.host);
const payload = true;
const payload = await SongsService.create(req.body.data, req.currentUser, true, link.host);
res.status(200).send(payload);
}));
@ -440,4 +438,4 @@ router.get('/:id', wrapAsync(async (req, res) => {
router.use('/', require('../helpers').commonErrorHandler);
module.exports = router;
module.exports = router;

View File

@ -1,5 +1,8 @@
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');
@ -7,15 +10,11 @@ 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 {
await Generation_jobsDBApi.create(
const job = await Generation_jobsDBApi.create(
data,
{
currentUser,
@ -24,12 +23,111 @@ module.exports = class Generation_jobsService {
);
await transaction.commit();
// Start asynchronous processing
this.processJob(job.id, currentUser).catch(err => {
console.error('Job processing failed:', err);
});
return job;
} catch (error) {
await transaction.rollback();
throw error;
}
};
static async processJob(jobId, currentUser) {
console.log(`Processing job ${jobId}...`);
try {
const job = await Generation_jobsDBApi.findBy({ id: jobId });
if (!job || !job.songId) return;
const song = await db.songs.findByPk(job.songId, {
include: [
{ model: db.languages, as: 'language' },
{ model: db.music_styles, as: 'style' },
{ model: db.eras, as: 'era' }
]
});
if (!song) return;
// Update job status to running
await Generation_jobsDBApi.update(jobId, {
status: 'running',
started_at: new Date()
}, { currentUser });
// Update song status to generating
await db.songs.update({ status: 'generating' }, { where: { id: song.id } });
// Simulate generation steps
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}".
Language: ${language}.
Music Style: ${style}.
Era: ${era}.
Include Verse 1, Chorus, Verse 2, Chorus, Bridge, and Final Chorus.`;
const aiResponse = await OpenAiService.askGpt(prompt);
if (aiResponse.success) {
await db.songs.update({ lyrics_text: aiResponse.data }, { where: { id: song.id } });
}
}
// Simulate "music composition" and "voice synthesis"
await Generation_jobsDBApi.update(jobId, { progress_percent: 50 }, { currentUser });
// Wait a bit to simulate processing time
await new Promise(resolve => setTimeout(resolve, 5000));
await Generation_jobsDBApi.update(jobId, { progress_percent: 100 }, { currentUser });
// Create a mock media asset so the UI shows it's playable
await Media_assetsDBApi.create({
songId: song.id,
asset_name: `${song.song_title} - Master`,
asset_type: 'audio_mp3',
mime_type: 'audio/mpeg',
duration_seconds: 180,
file_size_bytes: 5000000,
is_downloadable: true,
generated_at: new Date()
}, { currentUser });
// Update song status to ready
await db.songs.update({
status: 'ready',
completed_at: new Date()
}, { where: { id: song.id } });
// Mark job as succeeded
await Generation_jobsDBApi.update(jobId, {
status: 'succeeded',
finished_at: new Date()
}, { currentUser });
console.log(`Job ${jobId} finished successfully.`);
} catch (error) {
console.error(`Error processing job ${jobId}:`, error);
try {
await Generation_jobsDBApi.update(jobId, {
status: 'failed',
error_message: error.message
}, { currentUser });
await db.songs.update({ status: 'failed' }, { where: { id: song.id } });
} catch (innerError) {
console.error('Error updating status after failure:', innerError);
}
}
}
static async bulkImport(req, res, sendInvitationEmails = true, host) {
const transaction = await db.sequelize.transaction();
@ -131,8 +229,4 @@ module.exports = class Generation_jobsService {
throw error;
}
}
};

View File

@ -7,15 +7,11 @@ const axios = require('axios');
const config = require('../config');
const stream = require('stream');
module.exports = class SongsService {
static async create(data, currentUser) {
const transaction = await db.sequelize.transaction();
try {
await SongsDBApi.create(
const song = await SongsDBApi.create(
data,
{
currentUser,
@ -24,6 +20,7 @@ module.exports = class SongsService {
);
await transaction.commit();
return song;
} catch (error) {
await transaction.rollback();
throw error;
@ -131,8 +128,4 @@ module.exports = class SongsService {
throw error;
}
}
};
};

View File

@ -9,6 +9,7 @@ import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton
import BaseIcon from "../components/BaseIcon";
import { getPageTitle } from '../config'
import Link from "next/link";
import BaseButton from '../components/BaseButton';
import { hasPermission } from "../helpers/userPermissions";
import { fetchWidgets } from '../stores/roles/rolesSlice';
@ -103,7 +104,12 @@ const Dashboard = () => {
icon={icon.mdiChartTimelineVariant}
title='Overview'
main>
{''}
<BaseButton
href="/studio"
label="Open Music Studio"
icon={icon.mdiMusicNotePlus}
color="info"
/>
</SectionTitleLineWithButton>
{hasPermission(currentUser, 'CREATE_ROLES') && <WidgetCreator

View File

@ -1,7 +1,7 @@
import React, { ReactElement, useEffect, useState, useRef } from 'react';
import HeadInstance from 'next/head';
import { Formik, Form, Field } from 'formik';
import { mdiMusic, mdiMicrophone, mdiAutoFix, mdiHistory, mdiPlay, mdiDownload, mdiAlertCircle, mdiCheckCircle, mdiPause } from '@mdi/js';
import { mdiMusic, mdiMicrophone, mdiAutoFix, mdiHistory, mdiPlay, mdiDownload, mdiAlertCircle, mdiCheckCircle, mdiPause, mdiCreation, mdiAutoFix as mdiMagic } from '@mdi/js';
import LayoutAuthenticated from '../../layouts/Authenticated';
import SectionMain from '../../components/SectionMain';
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton';
@ -12,14 +12,17 @@ import { getPageTitle } from '../../config';
import { useAppDispatch, useAppSelector } from '../../stores/hooks';
import { fetch as fetchSongs, create as createSong } from '../../stores/songs/songsSlice';
import { create as createJob } from '../../stores/generation_jobs/generation_jobsSlice';
import { askGpt } from '../../stores/openAiSlice';
import { SelectField } from '../../components/SelectField';
import BaseIcon from '../../components/BaseIcon';
import FormField from '../../components/FormField';
import NotificationBar from '../../components/NotificationBar';
import axios from 'axios';
const StudioPage = () => {
const dispatch = useAppDispatch();
const { songs, loading: songsLoading } = useAppSelector((state) => state.songs);
const { isAskingQuestion: isGeneratingLyrics } = useAppSelector((state) => state.openAi);
const [isGenerating, setIsGenerating] = useState(false);
const [generationSuccess, setGenerationSuccess] = useState(false);
@ -51,6 +54,30 @@ const StudioPage = () => {
return errors;
};
const handleGenerateLyrics = async (values: any, setFieldValue: any) => {
if (!values.song_title || !values.styleId || !values.eraId) {
alert('Please provide a title, style, and era first.');
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].`;
try {
const result = await dispatch(askGpt(prompt)).unwrap();
if (result?.data) {
setFieldValue('lyrics_text', result.data);
}
} catch (error) {
console.error('Failed to generate lyrics:', error);
}
};
const handleSubmit = async (values: any, { resetForm }: any) => {
setIsGenerating(true);
try {
@ -65,8 +92,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: songResult.id,
songId: songId,
job_type: 'full_generation',
status: 'pending',
priority: 1,
@ -84,16 +116,24 @@ const StudioPage = () => {
};
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 (!asset || !asset.file_blob?.[0]) return;
const fileId = asset.file_blob[0].id;
const url = `/api/file/download?id=${fileId}`;
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);
return;
}
const fileId = asset.file_blob[0].id;
const url = `/api/file/download?id=${fileId}`;
if (audioRef.current) {
audioRef.current.src = url;
audioRef.current.play();
@ -104,7 +144,10 @@ 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]) return;
if (!asset || !asset.file_blob?.[0]) {
alert("Audio file is being prepared for download...");
return;
}
const fileId = asset.file_blob[0].id;
window.open(`/api/file/download?id=${fileId}`, '_blank');
@ -124,7 +167,12 @@ const StudioPage = () => {
<SectionMain>
<SectionTitleLineWithButton icon={mdiMusic} title="AI Music Generation Studio" main>
{''}
<BaseButton
href="/songs/songs-list"
label="Library"
icon={mdiHistory}
color="white"
/>
</SectionTitleLineWithButton>
{generationSuccess && (
@ -135,16 +183,21 @@ const StudioPage = () => {
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2">
<CardBox className="border-emerald-500/10">
<CardBox className="border-emerald-500/20 bg-slate-900/50">
<div className="mb-6 flex items-center space-x-2 text-emerald-400">
<BaseIcon path={mdiCreation} size={24} />
<h2 className="text-xl font-bold">Create New Masterpiece</h2>
</div>
<Formik initialValues={initialValues} validate={validate} onSubmit={handleSubmit}>
{({ values, errors, touched, setFieldValue }) => (
<Form className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<FormField label="Song Title" error={errors.song_title && touched.song_title}>
<FormField label="Song Title" error={errors.song_title && touched.song_title} help="Give your song a creative name">
<Field
name="song_title"
placeholder="My Epic AI Track"
className="w-full bg-slate-800 border-slate-700 text-white rounded-lg p-3 focus:ring-emerald-500"
placeholder="e.g., Midnight Shadows"
className="w-full bg-slate-800 border-slate-700 text-white rounded-lg p-3 focus:ring-emerald-500 border focus:border-emerald-500 outline-none transition-all"
/>
</FormField>
@ -152,16 +205,16 @@ const StudioPage = () => {
<Field
as="select"
name="generation_mode"
className="w-full bg-slate-800 border-slate-700 text-white rounded-lg p-3 focus:ring-emerald-500"
className="w-full bg-slate-800 border-slate-700 text-white rounded-lg p-3 focus:ring-emerald-500 border focus:border-emerald-500 outline-none transition-all"
>
<option value="manual_lyrics">Manual Lyrics</option>
<option value="auto_lyrics">AI Auto-Lyrics</option>
<option value="auto_lyrics">AI Auto-Lyrics (Full Song)</option>
<option value="remix_reference">Remix Reference</option>
</Field>
</FormField>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<FormField label="Language" error={errors.languageId && touched.languageId}>
<SelectField
itemRef="languages"
@ -169,66 +222,79 @@ const StudioPage = () => {
field={{ name: 'languageId', value: values.languageId }}
form={{ setFieldValue }}
options={{ id: 'languageId' }}
disabled={false}
/>
</FormField>
<FormField label="Music Era" error={errors.eraId && touched.eraId}>
<FormField label="Era" error={errors.eraId && touched.eraId}>
<SelectField
itemRef="eras"
showField="era_name"
field={{ name: 'eraId', value: values.eraId }}
form={{ setFieldValue }}
options={{ id: 'eraId' }}
disabled={false}
/>
</FormField>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<FormField label="Music Style" error={errors.styleId && touched.styleId}>
<FormField label="Style" error={errors.styleId && touched.styleId}>
<SelectField
itemRef="music_styles"
showField="style_name"
field={{ name: 'styleId', value: values.styleId }}
form={{ setFieldValue }}
options={{ id: 'styleId' }}
disabled={false}
/>
</FormField>
</div>
<FormField label="Vocal Character">
<FormField label="Vocal Character">
<SelectField
itemRef="ai_voices"
showField="voice_name"
field={{ name: 'voiceId', value: values.voiceId }}
form={{ setFieldValue }}
options={{ id: 'voiceId' }}
disabled={false}
/>
</FormField>
</div>
</FormField>
<FormField label="Lyrics" error={errors.lyrics_text && touched.lyrics_text}>
<div className="relative">
<div className="flex justify-between items-center mb-2">
<label className="block font-bold text-sm">Lyrics</label>
<BaseButton
type="button"
label={isGeneratingLyrics ? 'Writing...' : 'AI Generate Lyrics'}
icon={mdiMagic}
small
color="info"
onClick={() => handleGenerateLyrics(values, setFieldValue)}
disabled={isGeneratingLyrics || values.generation_mode === 'auto_lyrics'}
className="text-xs py-1 bg-indigo-600 hover:bg-indigo-700 border-none"
/>
</div>
<Field
as="textarea"
name="lyrics_text"
placeholder="Enter your lyrics here or let the AI generate them..."
className="w-full bg-slate-800 border-slate-700 text-white rounded-lg p-3 h-48 font-mono text-sm focus:ring-emerald-500"
placeholder={values.generation_mode === 'auto_lyrics' ? "AI will generate full lyrics during processing..." : "Enter your lyrics here..."}
className="w-full bg-slate-800 border-slate-700 text-white rounded-lg p-4 h-64 font-mono text-sm focus:ring-emerald-500 border focus:border-emerald-500 outline-none transition-all resize-none"
disabled={values.generation_mode === 'auto_lyrics'}
/>
</FormField>
{errors.lyrics_text && touched.lyrics_text && (
<div className="text-red-500 text-xs mt-1">{errors.lyrics_text}</div>
)}
</div>
<BaseButtons>
<div className="pt-4 border-t border-slate-800">
<BaseButton
type="submit"
color="info"
label={isGenerating ? 'Generating...' : 'Start Generation'}
label={isGenerating ? 'Initializing AI Engine...' : 'Generate Full Song'}
icon={mdiAutoFix}
className="w-full md:w-auto px-10 py-3 bg-emerald-500 hover:bg-emerald-600 text-slate-950 font-bold border-none transition-all hover:scale-105"
className="w-full py-4 bg-emerald-500 hover:bg-emerald-600 text-slate-950 font-black text-lg border-none shadow-lg shadow-emerald-500/20 transition-all hover:scale-[1.02] active:scale-[0.98]"
disabled={isGenerating}
/>
</BaseButtons>
<p className="text-[10px] text-center mt-3 text-slate-500 uppercase tracking-widest font-bold">
Lyrics Melody Arrangement AI Vocals Mix & Master
</p>
</div>
</Form>
)}
</Formik>
@ -236,63 +302,72 @@ const StudioPage = () => {
</div>
<div className="space-y-6">
<CardBox className="border-slate-800">
<div className="flex items-center justify-between mb-4">
<CardBox className="border-slate-800 bg-slate-900/30">
<div className="flex items-center justify-between mb-6">
<h3 className="text-lg font-bold text-white flex items-center">
<BaseIcon path={mdiHistory} className="mr-2 text-emerald-500" />
Recent Library
Recent Creations
</h3>
</div>
{songsLoading ? (
<div className="flex justify-center py-10">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-emerald-500" />
<div className="flex flex-col items-center justify-center py-20 space-y-4">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-emerald-500" />
<span className="text-xs text-slate-500 font-bold tracking-widest uppercase">Accessing Vault</span>
</div>
) : (
<div className="space-y-4">
{songs?.map((song: any) => {
const hasAudio = song.media_assets_song?.some((a: any) => a.asset_type.startsWith('audio'));
const isReady = song.status === 'ready';
return (
<div key={song.id} className="p-3 rounded-xl bg-slate-800/50 border border-slate-700 hover:border-emerald-500/50 transition-colors group">
<div className="flex items-center justify-between">
<div className="flex-1 min-w-0 mr-2">
<div className="font-bold text-sm text-white truncate">{song.song_title}</div>
<div className="text-[10px] text-slate-400 mt-1 uppercase tracking-wider">{song.style?.style_name} {song.era?.era_name}</div>
<div key={song.id} className="p-4 rounded-2xl bg-slate-800/40 border border-slate-700 hover:border-emerald-500/40 transition-all duration-300 group">
<div className="flex items-center justify-between mb-3">
<div className="flex-1 min-w-0 mr-4">
<div className="font-black text-sm text-white truncate group-hover:text-emerald-400 transition-colors">{song.song_title}</div>
<div className="flex items-center mt-1 space-x-2">
<span className="text-[9px] px-1.5 py-0.5 rounded bg-slate-700 text-slate-300 font-bold uppercase tracking-tighter">
{song.style?.style_name || 'Genre'}
</span>
<span className="text-[9px] text-slate-500 font-bold uppercase tracking-tighter">
{song.era?.era_name || 'Era'}
</span>
</div>
</div>
<div className="flex space-x-1 shrink-0">
{hasAudio && (
<BaseButton
<div className="flex space-x-2 shrink-0">
<BaseButton
icon={playingSongId === song.id ? mdiPause : mdiPlay}
color="info"
small
roundedFull
onClick={() => togglePlay(song)}
className="bg-emerald-500 hover:bg-emerald-600 text-slate-950 border-none"
/>
)}
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`}
/>
<BaseButton
icon={mdiDownload}
color="white"
small
roundedFull
onClick={() => handleDownload(song)}
disabled={!song.media_assets_song?.length}
disabled={!isReady}
className="bg-slate-700 hover:bg-slate-600 text-white border-none"
/>
</div>
</div>
<div className="mt-3">
<div className="flex items-center justify-between text-[10px] mb-1">
<span className="text-slate-500">Status</span>
<span className={`font-bold ${song.status === 'ready' ? 'text-emerald-400' : 'text-amber-400'}`}>
{song.status.toUpperCase()}
<div className="relative pt-1">
<div className="flex items-center justify-between text-[9px] mb-1 px-1">
<span className="text-slate-500 font-bold uppercase tracking-widest">Process Status</span>
<span className={`font-black uppercase tracking-widest ${isReady ? 'text-emerald-400' : 'text-amber-400 animate-pulse'}`}>
{song.status}
</span>
</div>
<div className="w-full bg-slate-900 rounded-full h-1">
<div className="w-full bg-slate-900 rounded-full h-1.5 overflow-hidden">
<div
className={`h-full rounded-full ${song.status === 'ready' ? 'bg-emerald-500' : 'bg-amber-500 animate-pulse'}`}
style={{ width: song.status === 'ready' ? '100%' : '60%' }}
className={`h-full rounded-full transition-all duration-1000 ${isReady ? 'bg-emerald-500' : 'bg-gradient-to-r from-amber-500 to-emerald-500 animate-pulse'}`}
style={{ width: isReady ? '100%' : '45%' }}
/>
</div>
</div>
@ -300,31 +375,32 @@ const StudioPage = () => {
);
})}
{songs?.length === 0 && (
<div className="text-center py-10 text-slate-500">
<BaseIcon path={mdiAlertCircle} size={48} className="mx-auto mb-2 opacity-20" />
<p>No tracks generated yet</p>
<div className="text-center py-20 text-slate-600">
<BaseIcon path={mdiMusic} size={64} className="mx-auto mb-4 opacity-10" />
<p className="font-bold tracking-widest uppercase text-xs">Studio Empty</p>
<p className="text-[10px] mt-2 italic">Your future hits will appear here</p>
</div>
)}
</div>
)}
<div className="mt-6 pt-6 border-t border-slate-700">
<div className="mt-8 pt-6 border-t border-slate-800">
<BaseButton
href="/songs/songs-list"
label="View Full Library"
label="Enter Audio Archive"
color="white"
className="w-full text-xs text-slate-400 bg-transparent border-slate-700 hover:bg-slate-800"
className="w-full text-[10px] font-black uppercase tracking-widest text-slate-500 bg-transparent border-slate-800 hover:bg-slate-800 hover:text-white transition-all"
/>
</div>
</CardBox>
<CardBox className="bg-gradient-to-br from-indigo-900/20 to-blue-900/20 border-blue-500/20">
<div className="flex items-center space-x-3 text-blue-400 mb-2">
<BaseIcon path={mdiMicrophone} size={24} />
<h4 className="font-bold">Studio Tip</h4>
<CardBox className="bg-gradient-to-br from-indigo-900/30 to-purple-900/30 border-indigo-500/20">
<div className="flex items-center space-x-3 text-indigo-400 mb-3">
<BaseIcon path={mdiMicrophone} size={28} />
<h4 className="font-black uppercase tracking-widest text-sm">Producer Tip</h4>
</div>
<p className="text-xs text-slate-400 leading-relaxed">
AI voices are more expressive when lyrics include punctuation. Try adding exclamation marks or ellipses for better vocal phrasing.
<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".
</p>
</CardBox>
</div>