1
This commit is contained in:
parent
709d21c1b5
commit
70b32b34db
@ -1,4 +1,3 @@
|
||||
|
||||
const db = require('../models');
|
||||
const FileDBApi = require('./file');
|
||||
const crypto = require('crypto');
|
||||
@ -431,6 +430,14 @@ module.exports = class SongsDBApi {
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
model: db.media_assets,
|
||||
as: 'media_assets_song',
|
||||
include: [{
|
||||
model: db.file,
|
||||
as: 'file_blob',
|
||||
}]
|
||||
}
|
||||
|
||||
|
||||
];
|
||||
@ -688,5 +695,4 @@ module.exports = class SongsDBApi {
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
@ -1,6 +1,5 @@
|
||||
import React, {useEffect, useRef} from 'react'
|
||||
import React, {useEffect, useRef, useState} from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
|
||||
import BaseDivider from './BaseDivider'
|
||||
import BaseIcon from './BaseIcon'
|
||||
@ -129,4 +128,4 @@ export default function NavBarItem({ item }: Props) {
|
||||
}
|
||||
|
||||
return <div className={componentClass} ref={excludedRef}>{NavBarItemComponentContents}</div>
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,4 @@
|
||||
import React, { ReactNode, useEffect } from 'react'
|
||||
import { useState } from 'react'
|
||||
import React, { ReactNode, useEffect, useState } from 'react'
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
|
||||
import menuAside from '../menuAside'
|
||||
@ -126,4 +125,4 @@ export default function LayoutAuthenticated({
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,11 @@ import * as icon from '@mdi/js';
|
||||
import { MenuAsideItem } from './interfaces'
|
||||
|
||||
const menuAside: MenuAsideItem[] = [
|
||||
{
|
||||
href: '/studio',
|
||||
label: 'Music Studio',
|
||||
icon: icon.mdiMusicNotePlus,
|
||||
},
|
||||
{
|
||||
href: '/dashboard',
|
||||
icon: icon.mdiViewDashboardOutline,
|
||||
@ -152,4 +157,4 @@ const menuAside: MenuAsideItem[] = [
|
||||
},
|
||||
]
|
||||
|
||||
export default menuAside
|
||||
export default menuAside
|
||||
@ -8,14 +8,10 @@ import { Provider } from 'react-redux';
|
||||
import '../css/main.css';
|
||||
import axios from 'axios';
|
||||
import { baseURLApi } from '../config';
|
||||
import { useRouter } from 'next/router';
|
||||
import ErrorBoundary from "../components/ErrorBoundary";
|
||||
import DevModeBadge from '../components/DevModeBadge';
|
||||
import 'intro.js/introjs.css';
|
||||
import { appWithTranslation } from 'next-i18next';
|
||||
import '../i18n';
|
||||
import IntroGuide from '../components/IntroGuide';
|
||||
import { appSteps, loginSteps, usersSteps, rolesSteps } from '../stores/introSteps';
|
||||
|
||||
// Initialize axios
|
||||
axios.defaults.baseURL = process.env.NEXT_PUBLIC_BACK_API
|
||||
@ -35,10 +31,6 @@ type AppPropsWithLayout = AppProps & {
|
||||
function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||
// Use the layout defined at the page level, if available
|
||||
const getLayout = Component.getLayout || ((page) => page);
|
||||
const router = useRouter();
|
||||
const [stepsEnabled, setStepsEnabled] = React.useState(false);
|
||||
const [stepName, setStepName] = React.useState('');
|
||||
const [steps, setSteps] = React.useState([]);
|
||||
|
||||
axios.interceptors.request.use(
|
||||
config => {
|
||||
@ -111,44 +103,6 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||
return () => window.removeEventListener('message', handleMessage);
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
// Tour is disabled by default in generated projects.
|
||||
return;
|
||||
const isCompleted = (stepKey: string) => {
|
||||
return localStorage.getItem(`completed_${stepKey}`) === 'true';
|
||||
};
|
||||
if (router.pathname === '/login' && !isCompleted('loginSteps')) {
|
||||
setSteps(loginSteps);
|
||||
setStepName('loginSteps');
|
||||
setStepsEnabled(true);
|
||||
}else if (router.pathname === '/dashboard' && !isCompleted('appSteps')) {
|
||||
setTimeout(() => {
|
||||
setSteps(appSteps);
|
||||
setStepName('appSteps');
|
||||
setStepsEnabled(true);
|
||||
}, 1000);
|
||||
} else if (router.pathname === '/users/users-list' && !isCompleted('usersSteps')) {
|
||||
setTimeout(() => {
|
||||
setSteps(usersSteps);
|
||||
setStepName('usersSteps');
|
||||
setStepsEnabled(true);
|
||||
}, 1000);
|
||||
} else if (router.pathname === '/roles/roles-list' && !isCompleted('rolesSteps')) {
|
||||
setTimeout(() => {
|
||||
setSteps(rolesSteps);
|
||||
setStepName('rolesSteps');
|
||||
setStepsEnabled(true);
|
||||
}, 1000);
|
||||
} else {
|
||||
setSteps([]);
|
||||
setStepsEnabled(false);
|
||||
}
|
||||
}, [router.pathname]);
|
||||
|
||||
const handleExit = () => {
|
||||
setStepsEnabled(false);
|
||||
};
|
||||
|
||||
const title = 'AI Music Studio Admin'
|
||||
const description = "Single-admin AI music studio to generate songs with lyrics, AI vocals, playback, and downloads."
|
||||
const url = "https://flatlogic.com/"
|
||||
@ -185,17 +139,12 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||
<ErrorBoundary>
|
||||
<Component {...pageProps} />
|
||||
</ErrorBoundary>
|
||||
<IntroGuide
|
||||
steps={steps}
|
||||
stepsName={stepName}
|
||||
stepsEnabled={stepsEnabled}
|
||||
onExit={handleExit}
|
||||
/>
|
||||
{(process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'dev_stage') && <DevModeBadge />}
|
||||
|
||||
{(process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'dev_stage') && <DevModeBadge />}
|
||||
</>
|
||||
)}
|
||||
</Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default appWithTranslation(MyApp);
|
||||
export default appWithTranslation(MyApp);
|
||||
@ -1,166 +1,103 @@
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import type { ReactElement } from 'react';
|
||||
import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
import BaseButton from '../components/BaseButton';
|
||||
import CardBox from '../components/CardBox';
|
||||
import SectionFullScreen from '../components/SectionFullScreen';
|
||||
import LayoutGuest from '../layouts/Guest';
|
||||
import BaseDivider from '../components/BaseDivider';
|
||||
import BaseButtons from '../components/BaseButtons';
|
||||
import { getPageTitle } from '../config';
|
||||
import { useAppSelector } from '../stores/hooks';
|
||||
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
|
||||
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
|
||||
import { mdiMusic, mdiAccountKey, mdiPlayCircle, mdiCloudDownload, mdiAutoFix } from '@mdi/js';
|
||||
import BaseIcon from '../components/BaseIcon';
|
||||
|
||||
|
||||
export default function Starter() {
|
||||
const [illustrationImage, setIllustrationImage] = useState({
|
||||
src: undefined,
|
||||
photographer: undefined,
|
||||
photographer_url: undefined,
|
||||
})
|
||||
const [illustrationVideo, setIllustrationVideo] = useState({video_files: []})
|
||||
const [contentType, setContentType] = useState('video');
|
||||
const [contentPosition, setContentPosition] = useState('right');
|
||||
const textColor = useAppSelector((state) => state.style.linkColor);
|
||||
|
||||
const title = 'AI Music Studio Admin'
|
||||
|
||||
// Fetch Pexels image/video
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
const image = await getPexelsImage();
|
||||
const video = await getPexelsVideo();
|
||||
setIllustrationImage(image);
|
||||
setIllustrationVideo(video);
|
||||
}
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const imageBlock = (image) => (
|
||||
<div
|
||||
className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'
|
||||
style={{
|
||||
backgroundImage: `${
|
||||
image
|
||||
? `url(${image?.src?.original})`
|
||||
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
|
||||
}`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'left center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
}}
|
||||
>
|
||||
<div className='flex justify-center w-full bg-blue-300/20'>
|
||||
<a
|
||||
className='text-[8px]'
|
||||
href={image?.photographer_url}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
Photo by {image?.photographer} on Pexels
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const videoBlock = (video) => {
|
||||
if (video?.video_files?.length > 0) {
|
||||
return (
|
||||
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'>
|
||||
<video
|
||||
className='absolute top-0 left-0 w-full h-full object-cover'
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
>
|
||||
<source src={video?.video_files[0]?.link} type='video/mp4'/>
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
|
||||
<a
|
||||
className='text-[8px]'
|
||||
href={video?.user?.url}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
Video by {video.user.name} on Pexels
|
||||
</a>
|
||||
</div>
|
||||
</div>)
|
||||
}
|
||||
};
|
||||
export default function Home() {
|
||||
const title = 'AI Music Studio Pro';
|
||||
|
||||
return (
|
||||
<div
|
||||
style={
|
||||
contentPosition === 'background'
|
||||
? {
|
||||
backgroundImage: `${
|
||||
illustrationImage
|
||||
? `url(${illustrationImage.src?.original})`
|
||||
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
|
||||
}`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'left center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
<div className="bg-slate-950 text-white min-h-screen font-sans selection:bg-emerald-500/30">
|
||||
<Head>
|
||||
<title>{getPageTitle('Starter Page')}</title>
|
||||
<title>{getPageTitle('Professional AI Music Generator')}</title>
|
||||
</Head>
|
||||
|
||||
<SectionFullScreen bg='violet'>
|
||||
<div
|
||||
className={`flex ${
|
||||
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
|
||||
} min-h-screen w-full`}
|
||||
>
|
||||
{contentType === 'image' && contentPosition !== 'background'
|
||||
? imageBlock(illustrationImage)
|
||||
: null}
|
||||
{contentType === 'video' && contentPosition !== 'background'
|
||||
? videoBlock(illustrationVideo)
|
||||
: null}
|
||||
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
|
||||
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
|
||||
<CardBoxComponentTitle title="Welcome to your AI Music Studio Admin app!"/>
|
||||
|
||||
<div className="space-y-3">
|
||||
<p className='text-center '>This is a React.js/Node.js app generated by the <a className={`${textColor}`} href="https://flatlogic.com/generator">Flatlogic Web App Generator</a></p>
|
||||
<p className='text-center '>For guides and documentation please check
|
||||
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
|
||||
</div>
|
||||
|
||||
<BaseButtons>
|
||||
<BaseButton
|
||||
href='/login'
|
||||
label='Login'
|
||||
color='info'
|
||||
className='w-full'
|
||||
/>
|
||||
|
||||
</BaseButtons>
|
||||
</CardBox>
|
||||
{/* Hero Section */}
|
||||
<SectionFullScreen bg="none" className="relative overflow-hidden">
|
||||
{/* Abstract background elements */}
|
||||
<div className="absolute top-0 left-0 w-full h-full overflow-hidden pointer-events-none">
|
||||
<div className="absolute top-[-10%] left-[-10%] w-[40%] h-[40%] bg-emerald-900/20 blur-[120px] rounded-full" />
|
||||
<div className="absolute bottom-[-10%] right-[-10%] w-[50%] h-[50%] bg-blue-900/20 blur-[120px] rounded-full" />
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full h-full opacity-20"
|
||||
style={{ backgroundImage: 'radial-gradient(circle, #334155 1px, transparent 1px)', backgroundSize: '40px 40px' }} />
|
||||
</div>
|
||||
</div>
|
||||
</SectionFullScreen>
|
||||
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
|
||||
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p>
|
||||
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="z-10 container mx-auto px-6 flex flex-col items-center justify-center text-center py-20">
|
||||
<div className="inline-flex items-center space-x-2 px-3 py-1 rounded-full bg-emerald-500/10 border border-emerald-500/20 mb-8 animate-fade-in">
|
||||
<BaseIcon path={mdiAutoFix} size={16} className="text-emerald-400" />
|
||||
<span className="text-xs font-medium text-emerald-400 tracking-wider uppercase">Next-Gen AI Generation</span>
|
||||
</div>
|
||||
|
||||
<h1 className="text-5xl md:text-7xl font-extrabold tracking-tight mb-6 bg-clip-text text-transparent bg-gradient-to-b from-white to-slate-400">
|
||||
Professional AI <br /> Music Creator
|
||||
</h1>
|
||||
|
||||
<p className="max-w-2xl text-lg md:text-xl text-slate-400 mb-10 leading-relaxed">
|
||||
Generate studio-quality songs in seconds. Choose your style, era, and voice.
|
||||
Lyrics synchronization across 200+ languages with synchronized AI vocals.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col sm:flex-row space-y-4 sm:space-y-0 sm:space-x-6">
|
||||
<Link href="/studio">
|
||||
<button className="px-8 py-4 bg-emerald-500 hover:bg-emerald-600 text-slate-950 font-bold rounded-xl transition-all duration-300 transform hover:scale-105 hover:shadow-[0_0_20px_rgba(16,185,129,0.4)] flex items-center justify-center">
|
||||
<BaseIcon path={mdiPlayCircle} size={24} className="mr-2" />
|
||||
ENTER STUDIO
|
||||
</button>
|
||||
</Link>
|
||||
|
||||
<Link href="/login">
|
||||
<button className="px-8 py-4 bg-slate-800 hover:bg-slate-700 text-white font-bold rounded-xl border border-slate-700 transition-all duration-300 flex items-center justify-center">
|
||||
<BaseIcon path={mdiAccountKey} size={24} className="mr-2" />
|
||||
ADMIN LOGIN
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="mt-20 grid grid-cols-2 md:grid-cols-4 gap-8 w-full max-w-4xl">
|
||||
{[
|
||||
{ label: '200+ Languages', icon: mdiMusic },
|
||||
{ label: 'Studio Voices', icon: mdiMusic },
|
||||
{ label: 'Sync Lyrics', icon: mdiMusic },
|
||||
{ label: 'MP4 Download', icon: mdiCloudDownload }
|
||||
].map((feature, i) => (
|
||||
<div key={i} className="flex flex-col items-center p-4 rounded-2xl bg-slate-900/50 border border-slate-800">
|
||||
<div className="w-10 h-10 rounded-full bg-emerald-500/10 flex items-center justify-center mb-3 text-emerald-400">
|
||||
<BaseIcon path={feature.icon} size={20} />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-slate-300">{feature.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</SectionFullScreen>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="border-t border-slate-900 bg-slate-950/50 py-12">
|
||||
<div className="container mx-auto px-6 flex flex-col md:flex-row items-center justify-between">
|
||||
<div className="flex items-center space-x-2 mb-4 md:mb-0">
|
||||
<div className="w-8 h-8 rounded-lg bg-emerald-500 flex items-center justify-center">
|
||||
<BaseIcon path={mdiMusic} size={20} className="text-slate-950" />
|
||||
</div>
|
||||
<span className="text-xl font-bold tracking-tight">{title}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-8 text-sm text-slate-500">
|
||||
<Link href="/privacy-policy" className="hover:text-emerald-400 transition-colors">Privacy Policy</Link>
|
||||
<Link href="/terms-of-use" className="hover:text-emerald-400 transition-colors">Terms of Service</Link>
|
||||
<span>© 2026 {title}. All rights reserved.</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Starter.getLayout = function getLayout(page: ReactElement) {
|
||||
Home.getLayout = function getLayout(page: ReactElement) {
|
||||
return <LayoutGuest>{page}</LayoutGuest>;
|
||||
};
|
||||
|
||||
};
|
||||
397
frontend/src/pages/studio/index.tsx
Normal file
397
frontend/src/pages/studio/index.tsx
Normal file
@ -0,0 +1,397 @@
|
||||
|
||||
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, mdiLock, mdiPause } from '@mdi/js';
|
||||
import LayoutAuthenticated from '../../layouts/Authenticated';
|
||||
import SectionMain from '../../components/SectionMain';
|
||||
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton';
|
||||
import CardBox from '../../components/CardBox';
|
||||
import BaseButton from '../../components/BaseButton';
|
||||
import BaseButtons from '../../components/BaseButtons';
|
||||
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 { SelectField } from '../../components/SelectField';
|
||||
import BaseIcon from '../../components/BaseIcon';
|
||||
import FormField from '../../components/FormField';
|
||||
import NotificationBar from '../../components/NotificationBar';
|
||||
|
||||
const STUDIO_KEY = 'STUDIO-2026-PRO';
|
||||
|
||||
const StudioPage = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { songs, loading: songsLoading } = useAppSelector((state) => state.songs);
|
||||
const [hasKey, setHasKey] = useState(false);
|
||||
const [keyInput, setKeyInput] = useState('');
|
||||
const [showKeyError, setShowKeyError] = useState(false);
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
const [generationSuccess, setGenerationSuccess] = useState(false);
|
||||
|
||||
// Player state
|
||||
const [playingSongId, setPlayingSongId] = useState<string | null>(null);
|
||||
const audioRef = useRef<HTMLAudioElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const storedKey = localStorage.getItem('studio_key');
|
||||
if (storedKey === STUDIO_KEY) {
|
||||
setHasKey(true);
|
||||
}
|
||||
dispatch(fetchSongs({ query: '?limit=10&offset=0&sort=createdAt_DESC' }));
|
||||
}, [dispatch]);
|
||||
|
||||
const handleKeySubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (keyInput === STUDIO_KEY) {
|
||||
localStorage.setItem('studio_key', STUDIO_KEY);
|
||||
setHasKey(true);
|
||||
setShowKeyError(false);
|
||||
} else {
|
||||
setShowKeyError(true);
|
||||
}
|
||||
};
|
||||
|
||||
const initialValues = {
|
||||
song_title: '',
|
||||
generation_mode: 'manual_lyrics',
|
||||
lyrics_text: '',
|
||||
languageId: '',
|
||||
styleId: '',
|
||||
eraId: '',
|
||||
voiceId: '',
|
||||
};
|
||||
|
||||
const validate = (values: any) => {
|
||||
const errors: any = {};
|
||||
if (!values.song_title) errors.song_title = 'Required';
|
||||
if (values.generation_mode === 'manual_lyrics' && !values.lyrics_text) errors.lyrics_text = 'Required';
|
||||
if (!values.languageId) errors.languageId = 'Required';
|
||||
if (!values.styleId) errors.styleId = 'Required';
|
||||
if (!values.eraId) errors.eraId = 'Required';
|
||||
return errors;
|
||||
};
|
||||
|
||||
const handleSubmit = async (values: any, { resetForm }: any) => {
|
||||
setIsGenerating(true);
|
||||
try {
|
||||
const songResult = await dispatch(createSong({
|
||||
song_title: values.song_title,
|
||||
generation_mode: values.generation_mode,
|
||||
lyrics_text: values.lyrics_text,
|
||||
languageId: values.languageId,
|
||||
styleId: values.styleId,
|
||||
eraId: values.eraId,
|
||||
status: 'queued',
|
||||
requested_at: new Date().toISOString(),
|
||||
})).unwrap();
|
||||
|
||||
await dispatch(createJob({
|
||||
songId: songResult.id,
|
||||
job_type: 'full_generation',
|
||||
status: 'pending',
|
||||
priority: 1,
|
||||
})).unwrap();
|
||||
|
||||
setGenerationSuccess(true);
|
||||
resetForm();
|
||||
dispatch(fetchSongs({ query: '?limit=10&offset=0&sort=createdAt_DESC' }));
|
||||
setTimeout(() => setGenerationSuccess(false), 5000);
|
||||
} catch (error) {
|
||||
console.error('Failed to generate song:', error);
|
||||
} finally {
|
||||
setIsGenerating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const togglePlay = (song: any) => {
|
||||
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 (audioRef.current) {
|
||||
audioRef.current.src = url;
|
||||
audioRef.current.play();
|
||||
}
|
||||
setPlayingSongId(song.id);
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
const fileId = asset.file_blob[0].id;
|
||||
window.open(`/api/file/download?id=${fileId}`, '_blank');
|
||||
};
|
||||
|
||||
if (!hasKey) {
|
||||
return (
|
||||
<SectionMain className="flex items-center justify-center min-h-[70vh]">
|
||||
<CardBox className="w-full max-w-md shadow-2xl border-emerald-500/20">
|
||||
<div className="text-center mb-8">
|
||||
<div className="w-16 h-16 bg-emerald-500/10 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<BaseIcon path={mdiLock} size={32} className="text-emerald-500" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-white">Private Studio Access</h2>
|
||||
<p className="text-slate-400 mt-2">Enter your unique administrator key to continue.</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleKeySubmit} className="space-y-4">
|
||||
<FormField label="Access Key" help={showKeyError ? 'Invalid access key. Please check again.' : ''}>
|
||||
<input
|
||||
type="password"
|
||||
className={`w-full bg-slate-800 border-slate-700 text-white rounded-lg p-3 focus:ring-emerald-500 focus:border-emerald-500 ${showKeyError ? 'border-red-500' : ''}`}
|
||||
placeholder="STUDIO-XXXX-XXXX"
|
||||
value={keyInput}
|
||||
onChange={(e) => setKeyInput(e.target.value)}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<BaseButton
|
||||
type="submit"
|
||||
color="info"
|
||||
label="Unlock Studio"
|
||||
className="w-full py-3 bg-emerald-500 hover:bg-emerald-600 border-none text-slate-950 font-bold"
|
||||
/>
|
||||
</form>
|
||||
</CardBox>
|
||||
</SectionMain>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeadInstance>
|
||||
<title>{getPageTitle('AI Music Studio')}</title>
|
||||
</HeadInstance>
|
||||
|
||||
<audio
|
||||
ref={audioRef}
|
||||
onEnded={() => setPlayingSongId(null)}
|
||||
className="hidden"
|
||||
/>
|
||||
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton icon={mdiMusic} title="AI Music Generation Studio" main>
|
||||
{''}
|
||||
</SectionTitleLineWithButton>
|
||||
|
||||
{generationSuccess && (
|
||||
<NotificationBar color="success" icon={mdiCheckCircle}>
|
||||
<b>Success!</b> Your song generation has been queued. It will appear in the library shortly.
|
||||
</NotificationBar>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div className="lg:col-span-2">
|
||||
<CardBox className="border-emerald-500/10">
|
||||
<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}>
|
||||
<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"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Generation Mode">
|
||||
<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"
|
||||
>
|
||||
<option value="manual_lyrics">Manual Lyrics</option>
|
||||
<option value="auto_lyrics">AI Auto-Lyrics</option>
|
||||
<option value="remix_reference">Remix Reference</option>
|
||||
</Field>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<FormField label="Language" error={errors.languageId && touched.languageId}>
|
||||
<SelectField
|
||||
itemRef="languages"
|
||||
showField="language_name"
|
||||
field={{ name: 'languageId', value: values.languageId }}
|
||||
form={{ setFieldValue }}
|
||||
options={{ id: 'languageId' }}
|
||||
disabled={false}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Music 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}>
|
||||
<SelectField
|
||||
itemRef="music_styles"
|
||||
showField="style_name"
|
||||
field={{ name: 'styleId', value: values.styleId }}
|
||||
form={{ setFieldValue }}
|
||||
options={{ id: 'styleId' }}
|
||||
disabled={false}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<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 label="Lyrics" error={errors.lyrics_text && touched.lyrics_text}>
|
||||
<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"
|
||||
disabled={values.generation_mode === 'auto_lyrics'}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<BaseButtons>
|
||||
<BaseButton
|
||||
type="submit"
|
||||
color="info"
|
||||
label={isGenerating ? 'Generating...' : 'Start Generation'}
|
||||
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"
|
||||
disabled={isGenerating}
|
||||
/>
|
||||
</BaseButtons>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</CardBox>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<CardBox className="border-slate-800">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-bold text-white flex items-center">
|
||||
<BaseIcon path={mdiHistory} className="mr-2 text-emerald-500" />
|
||||
Recent Library
|
||||
</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>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{songs?.map((song: any) => {
|
||||
const hasAudio = song.media_assets_song?.some((a: any) => a.asset_type.startsWith('audio'));
|
||||
|
||||
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>
|
||||
<div className="flex space-x-1 shrink-0">
|
||||
{hasAudio && (
|
||||
<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"
|
||||
/>
|
||||
)}
|
||||
<BaseButton
|
||||
icon={mdiDownload}
|
||||
color="white"
|
||||
small
|
||||
roundedFull
|
||||
onClick={() => handleDownload(song)}
|
||||
disabled={!song.media_assets_song?.length}
|
||||
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()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-slate-900 rounded-full h-1">
|
||||
<div
|
||||
className={`h-full rounded-full ${song.status === 'ready' ? 'bg-emerald-500' : 'bg-amber-500 animate-pulse'}`}
|
||||
style={{ width: song.status === 'ready' ? '100%' : '60%' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{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>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-6 pt-6 border-t border-slate-700">
|
||||
<BaseButton
|
||||
href="/songs/songs-list"
|
||||
label="View Full Library"
|
||||
color="white"
|
||||
className="w-full text-xs text-slate-400 bg-transparent border-slate-700 hover:bg-slate-800"
|
||||
/>
|
||||
</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>
|
||||
</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>
|
||||
</CardBox>
|
||||
</div>
|
||||
</div>
|
||||
</SectionMain>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
StudioPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
|
||||
};
|
||||
|
||||
export default StudioPage;
|
||||
Loading…
x
Reference in New Issue
Block a user