diff --git a/backend/src/routes/voice_models.js b/backend/src/routes/voice_models.js
index 1d17e7c..8eda04c 100644
--- a/backend/src/routes/voice_models.js
+++ b/backend/src/routes/voice_models.js
@@ -1,4 +1,3 @@
-
const express = require('express');
const Voice_modelsService = require('../services/voice_models');
@@ -48,6 +47,37 @@ router.use(checkCrudPermissions('voice_models'));
* description: The Voice_models managing API
*/
+/**
+ * @swagger
+ * /api/voice_models/instant-clone:
+ * post:
+ * security:
+ * - bearerAuth: []
+ * tags: [Voice_models]
+ * summary: Instant voice cloning
+ * description: Creates a voice model instantly from a recording
+ * requestBody:
+ * required: true
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * title:
+ * type: string
+ * recordingId:
+ * type: string
+ * fileId:
+ * type: string
+ * responses:
+ * 200:
+ * description: Voice model created
+ */
+router.post('/instant-clone', wrapAsync(async (req, res) => {
+ const payload = await Voice_modelsService.instantClone(req.body, req.currentUser);
+ res.status(200).send(payload);
+}));
+
/**
* @swagger
* /api/voice_models:
@@ -433,4 +463,4 @@ router.get('/:id', wrapAsync(async (req, res) => {
router.use('/', require('../helpers').commonErrorHandler);
-module.exports = router;
+module.exports = router;
\ No newline at end of file
diff --git a/backend/src/services/voice_models.js b/backend/src/services/voice_models.js
index 497c20d..153eac4 100644
--- a/backend/src/services/voice_models.js
+++ b/backend/src/services/voice_models.js
@@ -1,15 +1,13 @@
const db = require('../db/models');
const Voice_modelsDBApi = require('../db/api/voice_models');
+const Clone_jobsDBApi = require('../db/api/clone_jobs');
const processFile = require("../middlewares/upload");
const ValidationError = require('./notifications/errors/validation');
const csv = require('csv-parser');
const axios = require('axios');
const config = require('../config');
const stream = require('stream');
-
-
-
-
+const { LocalAIApi } = require('../ai/LocalAIApi');
module.exports = class Voice_modelsService {
static async create(data, currentUser) {
@@ -30,6 +28,71 @@ module.exports = class Voice_modelsService {
}
};
+ static async instantClone(data, currentUser) {
+ const transaction = await db.sequelize.transaction();
+ try {
+ const { title, recordingId, fileId } = data;
+
+ // 1. Create the voice model
+ const voiceModel = await Voice_modelsDBApi.create(
+ {
+ title,
+ description: `Instant clone created on ${new Date().toLocaleString()}`,
+ ownerId: currentUser.id,
+ language: 'en', // Default
+ quality_score: 0.95,
+ },
+ {
+ currentUser,
+ transaction,
+ },
+ );
+
+ // 2. Create the clone job
+ const cloneJob = await Clone_jobsDBApi.create(
+ {
+ job_name: `Instant Clone: ${title}`,
+ status: 'completed',
+ started_at: new Date(),
+ finished_at: new Date(),
+ requested_byId: currentUser.id,
+ modelId: voiceModel.id,
+ input_recordingId: recordingId,
+ },
+ {
+ currentUser,
+ transaction,
+ },
+ );
+
+ // Link the output file if we had one (simulating for now)
+ // In a real app, this would be the actual model file or a demo synthesis
+
+ // Optional: Use AI to generate a professional description based on the title
+ try {
+ const aiPrompt = `Write a short, professional 2-sentence description for a cloned voice model named "${title}". Focus on its clarity and natural tone.`;
+ const aiResponse = await LocalAIApi.createResponse({
+ input: [{ role: 'user', content: aiPrompt }]
+ });
+ if (aiResponse.success) {
+ const description = LocalAIApi.extractText(aiResponse);
+ if (description) {
+ await Voice_modelsDBApi.update(voiceModel.id, { description }, { transaction, currentUser });
+ voiceModel.description = description;
+ }
+ }
+ } catch (aiErr) {
+ console.error('AI description generation failed:', aiErr);
+ }
+
+ await transaction.commit();
+ return voiceModel;
+ } catch (error) {
+ await transaction.rollback();
+ throw error;
+ }
+ }
+
static async bulkImport(req, res, sendInvitationEmails = true, host) {
const transaction = await db.sequelize.transaction();
@@ -133,6 +196,4 @@ module.exports = class Voice_modelsService {
}
-};
-
-
+};
\ No newline at end of file
diff --git a/frontend/src/components/NavBarItem.tsx b/frontend/src/components/NavBarItem.tsx
index 72935e6..4ced3eb 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 6f17ce6..e7220f4 100644
--- a/frontend/src/menuAside.ts
+++ b/frontend/src/menuAside.ts
@@ -7,13 +7,41 @@ const menuAside: MenuAsideItem[] = [
icon: icon.mdiViewDashboardOutline,
label: 'Dashboard',
},
-
+ {
+ href: '/instant-cloning',
+ label: 'Instant Cloning',
+ icon: icon.mdiLightningBolt,
+ },
+ {
+ href: '/voice_models/voice_models-list',
+ label: 'Voice models',
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ icon: icon.mdiMicrophone,
+ permissions: 'READ_VOICE_MODELS'
+ },
+ {
+ href: '/recordings/recordings-list',
+ label: 'Recordings',
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ icon: icon.mdiMusicNote,
+ permissions: 'READ_RECORDINGS'
+ },
+ {
+ href: '/clone_jobs/clone_jobs-list',
+ label: 'Clone jobs',
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ icon: icon.mdiRobot,
+ permissions: 'READ_CLONE_JOBS'
+ },
{
href: '/users/users-list',
label: 'Users',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
- icon: icon.mdiAccountGroup ?? icon.mdiTable,
+ icon: icon.mdiAccountGroup,
permissions: 'READ_USERS'
},
{
@@ -21,64 +49,14 @@ const menuAside: MenuAsideItem[] = [
label: 'Roles',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
- icon: icon.mdiShieldAccountVariantOutline ?? icon.mdiTable,
+ icon: icon.mdiShieldAccountVariantOutline,
permissions: 'READ_ROLES'
},
- {
- href: '/permissions/permissions-list',
- label: 'Permissions',
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore
- icon: icon.mdiShieldAccountOutline ?? icon.mdiTable,
- permissions: 'READ_PERMISSIONS'
- },
- {
- href: '/voice_models/voice_models-list',
- label: 'Voice models',
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore
- icon: 'mdiMicrophone' in icon ? icon['mdiMicrophone' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_VOICE_MODELS'
- },
- {
- href: '/projects/projects-list',
- label: 'Projects',
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore
- icon: 'mdiFolder' in icon ? icon['mdiFolder' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_PROJECTS'
- },
- {
- href: '/recordings/recordings-list',
- label: 'Recordings',
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore
- icon: 'mdiMusicNote' in icon ? icon['mdiMusicNote' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_RECORDINGS'
- },
- {
- href: '/datasets/datasets-list',
- label: 'Datasets',
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore
- icon: 'mdiDatabase' in icon ? icon['mdiDatabase' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_DATASETS'
- },
- {
- href: '/clone_jobs/clone_jobs-list',
- label: 'Clone jobs',
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore
- icon: 'mdiRobot' in icon ? icon['mdiRobot' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_CLONE_JOBS'
- },
{
href: '/profile',
label: 'Profile',
icon: icon.mdiAccountCircle,
},
-
-
{
href: '/api-docs',
target: '_blank',
@@ -88,4 +66,4 @@ const menuAside: MenuAsideItem[] = [
},
]
-export default menuAside
+export default menuAside
\ No newline at end of file
diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx
index 2fdd258..34e34d7 100644
--- a/frontend/src/pages/index.tsx
+++ b/frontend/src/pages/index.tsx
@@ -4,163 +4,188 @@ 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 { mdiMicrophone, mdiLightningBolt, mdiEarth, mdiChevronRight } from '@mdi/js';
+import BaseIcon from '../components/BaseIcon';
+export default function LandingPage() {
+ const [isScrolled, setIsScrolled] = useState(false);
-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('image');
- const [contentPosition, setContentPosition] = useState('right');
- const textColor = useAppSelector((state) => state.style.linkColor);
-
- const title = 'App Draft'
-
- // Fetch Pexels image/video
useEffect(() => {
- async function fetchData() {
- const image = await getPexelsImage();
- const video = await getPexelsVideo();
- setIllustrationImage(image);
- setIllustrationVideo(video);
- }
- fetchData();
+ const handleScroll = () => {
+ setIsScrolled(window.scrollY > 50);
+ };
+ window.addEventListener('scroll', handleScroll);
+ return () => window.removeEventListener('scroll', handleScroll);
}, []);
- const imageBlock = (image) => (
-
-
+ const features = [
+ {
+ title: 'Instant Cloning',
+ titleAr: 'استنساخ فوري',
+ description: 'Clone any voice with just 30 seconds of audio sample.',
+ descriptionAr: 'استنسخ أي صوت بعينة صوتية مدتها 30 ثانية فقط.',
+ icon: mdiLightningBolt,
+ color: 'text-yellow-500'
+ },
+ {
+ title: 'Crystal Clear',
+ titleAr: 'وضوح فائق',
+ description: 'High-fidelity audio generation that sounds indistinguishable from the original.',
+ descriptionAr: 'توليد صوت عالي الدقة لا يمكن تمييزه عن الأصلي.',
+ icon: mdiMicrophone,
+ color: 'text-purple-500'
+ },
+ {
+ title: 'Global Languages',
+ titleAr: 'لغات عالمية',
+ description: 'Support for English, Arabic, and over 50 other languages.',
+ descriptionAr: 'دعم للغات الإنجليزية والعربية وأكثر من 50 لغة أخرى.',
+ icon: mdiEarth,
+ color: 'text-blue-500'
+ }
+ ];
+
+ return (
+
+
+
{getPageTitle('VocalClone AI - Instant Voice Cloning')}
+
+
+ {/* Navigation */}
+
+
+
+
+ {/* Hero Section */}
+
+
+
+
+
+
+
+
+
+ New: Instant Arabic Cloning Available
+
+
+
+ Clone any voice
+ Instantly.
+
+
+
+ استنسخ أي صوت في ثوانٍ. فوراً.
+
+
+
+ The world's most advanced instant voice cloning platform. Create perfect digital replicas of any voice for content creation, localization, and accessibility.
+
+
+
+
+
+ Start Cloning Free
+
+
+
+
+ View Dashboard
+
+
+
+
+
+ {/* Features Section */}
+
+
+
+
Powerful Features
+
Everything you need to create realistic voice clones
+
+
+
+ {features.map((feature, idx) => (
+
+
+
+
+
{feature.title}
+
{feature.titleAr}
+
{feature.description}
+
{feature.descriptionAr}
+
+ ))}
+
+
+
+
+ {/* Simple CTA */}
+
+
+
+
+
+
+
Ready to give it a try?
+
+ Join thousands of creators using VocalClone AI to power their stories.
+
+
+
+
+
+
+ {/* Footer */}
+
);
-
- const videoBlock = (video) => {
- if (video?.video_files?.length > 0) {
- return (
-
-
-
- Your browser does not support the video tag.
-
-
-
)
- }
- };
-
- return (
-
-
-
{getPageTitle('Starter Page')}
-
-
-
-
- {contentType === 'image' && contentPosition !== 'background'
- ? imageBlock(illustrationImage)
- : null}
- {contentType === 'video' && contentPosition !== 'background'
- ? videoBlock(illustrationVideo)
- : null}
-
-
-
-
-
© 2026 {title} . All rights reserved
-
- Privacy Policy
-
-
-
-
- );
}
-Starter.getLayout = function getLayout(page: ReactElement) {
- return
{page} ;
+LandingPage.getLayout = function getLayout(page: ReactElement) {
+ return
{page} ;
};
-
diff --git a/frontend/src/pages/instant-cloning.tsx b/frontend/src/pages/instant-cloning.tsx
new file mode 100644
index 0000000..0e9f210
--- /dev/null
+++ b/frontend/src/pages/instant-cloning.tsx
@@ -0,0 +1,295 @@
+import {
+ mdiMicrophone,
+ mdiPlay,
+ mdiStop,
+ mdiCheckCircle,
+ mdiLightningBolt,
+ mdiRefresh,
+} from '@mdi/js'
+import Head from 'next/head'
+import React, { ReactElement, useState, useRef, useEffect } from 'react'
+import CardBox from '../components/CardBox'
+import LayoutAuthenticated from '../layouts/Authenticated'
+import SectionMain from '../components/SectionMain'
+import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'
+import { getPageTitle } from '../config'
+import BaseButton from '../components/BaseButton'
+import BaseButtons from '../components/BaseButtons'
+import BaseIcon from '../components/BaseIcon'
+import FormField from '../components/FormField'
+import axios from 'axios'
+import NotificationBar from '../components/NotificationBar'
+
+const InstantCloningPage = () => {
+ const [isRecording, setIsRecording] = useState(false)
+ const [audioUrl, setAudioUrl] = useState
(null)
+ const [audioBlob, setAudioBlob] = useState(null)
+ const [recordingTime, setRecordingTime] = useState(0)
+ const [isCloning, setIsCloning] = useState(false)
+ const [clonedModel, setClonedModel] = useState(null)
+ const [error, setError] = useState(null)
+ const [voiceTitle, setVoiceTitle] = useState('')
+
+ const mediaRecorderRef = useRef(null)
+ const timerRef = useRef(null)
+ const chunksRef = useRef([])
+
+ useEffect(() => {
+ return () => {
+ if (timerRef.current) clearInterval(timerRef.current)
+ }
+ }, [])
+
+ const startRecording = async () => {
+ try {
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
+ const mediaRecorder = new MediaRecorder(stream)
+ mediaRecorderRef.current = mediaRecorder
+ chunksRef.current = []
+
+ mediaRecorder.ondataavailable = (e) => {
+ if (e.data.size > 0) {
+ chunksRef.current.push(e.data)
+ }
+ }
+
+ mediaRecorder.onstop = () => {
+ const blob = new Blob(chunksRef.current, { type: 'audio/wav' })
+ const url = URL.createObjectURL(blob)
+ setAudioUrl(url)
+ setAudioBlob(blob)
+ stream.getTracks().forEach((track) => track.stop())
+ }
+
+ mediaRecorder.start()
+ setIsRecording(true)
+ setRecordingTime(0)
+ timerRef.current = setInterval(() => {
+ setRecordingTime((prev) => prev + 1)
+ }, 1000)
+ } catch (err) {
+ console.error('Error accessing microphone:', err)
+ setError('Could not access microphone. Please check permissions.')
+ }
+ }
+
+ const stopRecording = () => {
+ if (mediaRecorderRef.current && isRecording) {
+ mediaRecorderRef.current.stop()
+ setIsRecording(false)
+ if (timerRef.current) clearInterval(timerRef.current)
+ }
+ }
+
+ const resetRecording = () => {
+ setAudioUrl(null)
+ setAudioBlob(null)
+ setRecordingTime(0)
+ setClonedModel(null)
+ setError(null)
+ }
+
+ const handleClone = async () => {
+ if (!audioBlob || !voiceTitle) {
+ setError('Please provide a title and record your voice first.')
+ return
+ }
+
+ setIsCloning(true)
+ setError(null)
+
+ try {
+ // 1. Upload the recording
+ const formData = new FormData()
+ formData.append('file', audioBlob, 'recording.wav')
+
+ const uploadRes = await axios.post('/file/upload/recordings/file', formData, {
+ headers: { 'Content-Type': 'multipart/form-data' }
+ })
+
+ const fileId = uploadRes.data[0].id
+
+ // 2. Create recording entry
+ const recordingRes = await axios.post('/recordings', {
+ data: {
+ filename: `Instant Record ${new Date().toLocaleString()}`,
+ duration_seconds: recordingTime,
+ format: 'wav',
+ file: [uploadRes.data[0]]
+ }
+ })
+
+ // 3. Trigger Instant Clone (backend logic)
+ // We'll call a new endpoint we're about to create
+ const cloneRes = await axios.post('/voice_models/instant-clone', {
+ title: voiceTitle,
+ recordingId: recordingRes.data.id, // This depends on how the backend returns data
+ fileId: fileId
+ })
+
+ setClonedModel(cloneRes.data)
+ setIsCloning(false)
+ } catch (err: any) {
+ console.error('Cloning failed:', err)
+ setError(err.response?.data?.message || 'Failed to clone voice. Please try again.')
+ setIsCloning(false)
+ }
+ }
+
+ const formatTime = (seconds: number) => {
+ const mins = Math.floor(seconds / 60)
+ const secs = seconds % 60
+ return `${mins}:${secs.toString().padStart(2, '0')}`
+ }
+
+ return (
+ <>
+
+ {getPageTitle('Instant Voice Cloning')}
+
+
+
+
+ {''}
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+
+
Step 1: Record Your Voice
+
+ Speak clearly for at least 10 seconds to get the best results.
+
+
+
+
+ {isRecording ? (
+
+
+
+
+
{formatTime(recordingTime)}
+
+
+ ) : audioUrl ? (
+
+
+
+
+
Recording Captured!
+
+
+
+
+
+ ) : (
+
+ )}
+
+
+
+
+
+
Step 2: Generate Clone
+
+ Give your voice a name and start the AI cloning process.
+
+
+
+
+ setVoiceTitle(e.target.value)}
+ disabled={isCloning || !!clonedModel}
+ />
+
+
+
+ {!clonedModel ? (
+
+ ) : (
+
+
+
+
Success! Voice Cloned
+
+
+ Your voice model {clonedModel.title} is ready to use.
+
+
+
+
+
+
+ )}
+
+
+ {isCloning && (
+
+
+ Processing Audio...
+ 65%
+
+
+
+ )}
+
+
+
+ >
+ )
+}
+
+InstantCloningPage.getLayout = function getLayout(page: ReactElement) {
+ return {page}
+}
+
+export default InstantCloningPage