diff --git a/backend/src/db/api/songs.js b/backend/src/db/api/songs.js
index 9d38bd1..59ef6b0 100644
--- a/backend/src/db/api/songs.js
+++ b/backend/src/db/api/songs.js
@@ -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 {
}
-};
-
+};
\ No newline at end of file
diff --git a/frontend/src/components/NavBarItem.tsx b/frontend/src/components/NavBarItem.tsx
index eb155e3..1986306 100644
--- a/frontend/src/components/NavBarItem.tsx
+++ b/frontend/src/components/NavBarItem.tsx
@@ -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
{NavBarItemComponentContents}
-}
+}
\ No newline at end of file
diff --git a/frontend/src/layouts/Authenticated.tsx b/frontend/src/layouts/Authenticated.tsx
index 1b9907d..26c3572 100644
--- a/frontend/src/layouts/Authenticated.tsx
+++ b/frontend/src/layouts/Authenticated.tsx
@@ -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({
)
-}
+}
\ No newline at end of file
diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts
index 5840292..3fb26ad 100644
--- a/frontend/src/menuAside.ts
+++ b/frontend/src/menuAside.ts
@@ -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
\ No newline at end of file
diff --git a/frontend/src/pages/_app.tsx b/frontend/src/pages/_app.tsx
index 2f52f84..05cac9e 100644
--- a/frontend/src/pages/_app.tsx
+++ b/frontend/src/pages/_app.tsx
@@ -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) {
-
- {(process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'dev_stage') && }
+
+ {(process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'dev_stage') && }
>
)}
)
}
-export default appWithTranslation(MyApp);
+export default appWithTranslation(MyApp);
\ No newline at end of file
diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx
index fab6045..9590ae2 100644
--- a/frontend/src/pages/index.tsx
+++ b/frontend/src/pages/index.tsx
@@ -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) => (
-
- );
-
- const videoBlock = (video) => {
- if (video?.video_files?.length > 0) {
- return (
-
-
-
- Your browser does not support the video tag.
-
-
-
)
- }
- };
+export default function Home() {
+ const title = 'AI Music Studio Pro';
return (
-
+
-
{getPageTitle('Starter Page')}
+
{getPageTitle('Professional AI Music Generator')}
-
-
- {contentType === 'image' && contentPosition !== 'background'
- ? imageBlock(illustrationImage)
- : null}
- {contentType === 'video' && contentPosition !== 'background'
- ? videoBlock(illustrationVideo)
- : null}
-
-
-
-
-
-
-
-
-
-
-
+ {/* Hero Section */}
+
+ {/* Abstract background elements */}
+
-
-
-
-
© 2026 {title} . All rights reserved
-
- Privacy Policy
-
-
+
+
+
+ Next-Gen AI Generation
+
+
+
+ Professional AI Music Creator
+
+
+
+ Generate studio-quality songs in seconds. Choose your style, era, and voice.
+ Lyrics synchronization across 200+ languages with synchronized AI vocals.
+
+
+
+
+
+
+ ENTER STUDIO
+
+
+
+
+
+
+ ADMIN LOGIN
+
+
+
+
+
+ {[
+ { label: '200+ Languages', icon: mdiMusic },
+ { label: 'Studio Voices', icon: mdiMusic },
+ { label: 'Sync Lyrics', icon: mdiMusic },
+ { label: 'MP4 Download', icon: mdiCloudDownload }
+ ].map((feature, i) => (
+
+
+
+
+
{feature.label}
+
+ ))}
+
+
+
+
+ {/* Footer */}
+
);
}
-Starter.getLayout = function getLayout(page: ReactElement) {
+Home.getLayout = function getLayout(page: ReactElement) {
return {page} ;
-};
-
+};
\ No newline at end of file
diff --git a/frontend/src/pages/studio/index.tsx b/frontend/src/pages/studio/index.tsx
new file mode 100644
index 0000000..b62a655
--- /dev/null
+++ b/frontend/src/pages/studio/index.tsx
@@ -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(null);
+ const audioRef = useRef(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 (
+
+
+
+
+
+
+
Private Studio Access
+
Enter your unique administrator key to continue.
+
+
+
+
+
+ );
+ }
+
+ return (
+ <>
+
+ {getPageTitle('AI Music Studio')}
+
+
+ setPlayingSongId(null)}
+ className="hidden"
+ />
+
+
+
+ {''}
+
+
+ {generationSuccess && (
+
+ Success! Your song generation has been queued. It will appear in the library shortly.
+
+ )}
+
+
+
+
+
+ {({ values, errors, touched, setFieldValue }) => (
+
+ )}
+
+
+
+
+
+
+
+
+
+ Recent Library
+
+
+
+ {songsLoading ? (
+
+ ) : (
+
+ {songs?.map((song: any) => {
+ const hasAudio = song.media_assets_song?.some((a: any) => a.asset_type.startsWith('audio'));
+
+ return (
+
+
+
+
{song.song_title}
+
{song.style?.style_name} • {song.era?.era_name}
+
+
+ {hasAudio && (
+ togglePlay(song)}
+ className="bg-emerald-500 hover:bg-emerald-600 text-slate-950 border-none"
+ />
+ )}
+ handleDownload(song)}
+ disabled={!song.media_assets_song?.length}
+ className="bg-slate-700 hover:bg-slate-600 text-white border-none"
+ />
+
+
+
+
+ Status
+
+ {song.status.toUpperCase()}
+
+
+
+
+
+ );
+ })}
+ {songs?.length === 0 && (
+
+
+
No tracks generated yet
+
+ )}
+
+ )}
+
+
+
+
+
+
+
+
+
+
Studio Tip
+
+
+ AI voices are more expressive when lyrics include punctuation. Try adding exclamation marks or ellipses for better vocal phrasing.
+
+
+
+
+
+ >
+ );
+};
+
+StudioPage.getLayout = function getLayout(page: ReactElement) {
+ return {page} ;
+};
+
+export default StudioPage;