From 4063f71e4527c8b945a3169b1d8d5135e9c1354e Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Wed, 28 Jan 2026 00:23:00 +0000 Subject: [PATCH] =?UTF-8?q?=D8=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/routes/voice_models.js | 34 ++- backend/src/services/voice_models.js | 75 +++++- frontend/src/components/NavBarItem.tsx | 5 +- frontend/src/layouts/Authenticated.tsx | 5 +- frontend/src/menuAside.ts | 86 +++---- frontend/src/pages/index.tsx | 319 +++++++++++++------------ frontend/src/pages/instant-cloning.tsx | 295 +++++++++++++++++++++++ 7 files changed, 603 insertions(+), 216 deletions(-) create mode 100644 frontend/src/pages/instant-cloning.tsx 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) => ( -
-
- - Photo by {image?.photographer} on Pexels - -
+ 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 */} +
+
+
+
+ +
+ VocalClone AI +
+

© 2026 VocalClone AI. All rights reserved.

+
+ Privacy + Terms +
+
+
); - - const videoBlock = (video) => { - if (video?.video_files?.length > 0) { - return ( -
- -
- - Video by {video.user.name} on Pexels - -
-
) - } - }; - - return ( -
- - {getPageTitle('Starter Page')} - - - -
- {contentType === 'image' && contentPosition !== 'background' - ? imageBlock(illustrationImage) - : null} - {contentType === 'video' && contentPosition !== 'background' - ? videoBlock(illustrationVideo) - : null} -
- - - -
-

This is a React.js/Node.js app generated by the Flatlogic Web App Generator

-

For guides and documentation please check - your local README.md and the Flatlogic documentation

-
- - - - - -
-
-
-
-
-

© 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