3
This commit is contained in:
parent
f1be1e14ea
commit
d2e5447587
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
||||
};
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user