taghirsado / components /CustomVoice.tsx
Opera10's picture
Upload 10 files
a8df197 verified
Raw
History Blame Contribute Delete
13 kB
import React, { useState, useRef, useEffect } from 'react';
interface CustomVoiceProps {
onStart: (src: File, ref: File) => void;
isLoading: boolean;
}
const CustomVoice: React.FC<CustomVoiceProps> = ({ onStart, isLoading }) => {
const [srcFile, setSrcFile] = useState<File | null>(null);
const [refFile, setRefFile] = useState<File | null>(null);
// Audio Playback State
const [playingType, setPlayingType] = useState<'src' | 'ref' | null>(null);
const audioRef = useRef<HTMLAudioElement | null>(null);
const srcInputRef = useRef<HTMLInputElement>(null);
const refInputRef = useRef<HTMLInputElement>(null);
// Cleanup audio on unmount
useEffect(() => {
return () => {
if (audioRef.current) {
audioRef.current.pause();
audioRef.current = null;
}
};
}, []);
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>, type: 'src' | 'ref') => {
if (e.target.files && e.target.files[0]) {
// Stop playing if currently playing the file being changed
if (playingType === type && audioRef.current) {
audioRef.current.pause();
setPlayingType(null);
}
if (type === 'src') setSrcFile(e.target.files[0]);
else setRefFile(e.target.files[0]);
}
};
const togglePreview = (e: React.MouseEvent, type: 'src' | 'ref') => {
e.stopPropagation(); // Prevent opening file dialog
const file = type === 'src' ? srcFile : refFile;
if (!file) return;
// If clicking the currently playing type, stop it
if (playingType === type) {
if (audioRef.current) {
audioRef.current.pause();
audioRef.current = null;
}
setPlayingType(null);
return;
}
// Stop any other audio
if (audioRef.current) {
audioRef.current.pause();
}
// Create new audio
const url = URL.createObjectURL(file);
const audio = new Audio(url);
audioRef.current = audio;
audio.onended = () => {
setPlayingType(null);
URL.revokeObjectURL(url);
};
audio.onerror = () => {
alert("خطا در پخش فایل");
setPlayingType(null);
};
audio.play();
setPlayingType(type);
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (srcFile && refFile) {
if (audioRef.current) {
audioRef.current.pause();
setPlayingType(null);
}
onStart(srcFile, refFile);
}
};
return (
<div className="animate-[fadeIn_0.5s_ease-out]">
<div className="text-center mb-8">
<div className="w-16 h-16 bg-gradient-to-tr from-blue-500 to-purple-600 rounded-2xl mx-auto mb-4 flex items-center justify-center shadow-lg shadow-purple-500/20 rotate-3 transform hover:rotate-6 transition-transform">
<i className="fas fa-sliders-h text-2xl text-white"></i>
</div>
<h2 className="text-2xl font-black text-gray-800 mb-2">استودیو ساخت صدای اختصاصی</h2>
<p className="text-gray-700 text-sm font-bold mb-2 px-4 leading-relaxed">
با داشتن یک نمونه صدا هر صدایی رو به همون صدا تبدیل کنید
</p>
<p className="text-gray-500 text-xs px-6 leading-relaxed">
برای شبیه‌سازی دقیق، صدای خود و صدای هدف را با کیفیت بالا بارگذاری کنید.
</p>
</div>
<form onSubmit={handleSubmit} className="space-y-6 px-2">
{/* Source File */}
<div
onClick={() => srcInputRef.current?.click()}
className={`relative overflow-hidden rounded-3xl border-2 border-dashed p-6 transition-all duration-300 active:scale-95 cursor-pointer
${srcFile ? 'border-primary bg-blue-50/50' : 'border-gray-200 bg-white hover:bg-gray-50'}`}
>
<input
type="file"
ref={srcInputRef}
hidden
accept="audio/*"
onChange={(e) => handleFileChange(e, 'src')}
/>
<div className="flex items-center gap-4">
<button
type="button"
onClick={(e) => {
if (srcFile) togglePreview(e, 'src');
}}
className={`w-12 h-12 rounded-full flex items-center justify-center flex-shrink-0 transition-all duration-300 relative outline-none
${srcFile
? 'bg-primary text-white shadow-lg shadow-indigo-500/30 hover:scale-110 z-10'
: 'bg-gray-100 text-gray-400'}`}
>
{srcFile ? (
playingType === 'src' ? (
<>
<span className="absolute inset-0 rounded-full bg-white/30 animate-ping"></span>
<i className="fas fa-stop text-sm relative z-10"></i>
</>
) : (
<i className="fas fa-play ml-1 text-sm"></i>
)
) : (
<i className="fas fa-microphone text-xl"></i>
)}
</button>
<div className="text-right flex-1">
<h6 className="font-bold text-gray-700 text-sm">صدای ورودی (ویس شما)</h6>
<p className="text-xs text-gray-400 mt-1 truncate">
{srcFile ? srcFile.name : 'برای انتخاب ضربه بزنید'}
</p>
</div>
{srcFile && <i className="fas fa-check-circle text-primary text-xl"></i>}
</div>
</div>
{/* Reference File */}
<div
onClick={() => refInputRef.current?.click()}
className={`relative overflow-hidden rounded-3xl border-2 border-dashed p-6 transition-all duration-300 active:scale-95 cursor-pointer
${refFile ? 'border-secondary bg-purple-50/50' : 'border-gray-200 bg-white hover:bg-gray-50'}`}
>
<input
type="file"
ref={refInputRef}
hidden
accept="audio/*"
onChange={(e) => handleFileChange(e, 'ref')}
/>
<div className="flex items-center gap-4">
<button
type="button"
onClick={(e) => {
if (refFile) togglePreview(e, 'ref');
}}
className={`w-12 h-12 rounded-full flex items-center justify-center flex-shrink-0 transition-all duration-300 relative outline-none
${refFile
? 'bg-secondary text-white shadow-lg shadow-purple-500/30 hover:scale-110 z-10'
: 'bg-gray-100 text-gray-400'}`}
>
{refFile ? (
playingType === 'ref' ? (
<>
<span className="absolute inset-0 rounded-full bg-white/30 animate-ping"></span>
<i className="fas fa-stop text-sm relative z-10"></i>
</>
) : (
<i className="fas fa-play ml-1 text-sm"></i>
)
) : (
<i className="fas fa-music text-xl"></i>
)}
</button>
<div className="text-right flex-1">
<h6 className="font-bold text-gray-700 text-sm">صدای الگو (رفرنس)</h6>
<p className="text-xs text-gray-400 mt-1 truncate">
{refFile ? refFile.name : 'برای انتخاب ضربه بزنید'}
</p>
</div>
{refFile && <i className="fas fa-check-circle text-secondary text-xl"></i>}
</div>
</div>
<button
type="submit"
disabled={!srcFile || !refFile || isLoading}
className={`w-full py-4 rounded-2xl font-bold text-white shadow-xl shadow-purple-500/20 transition-all transform active:scale-95
${(!srcFile || !refFile || isLoading)
? 'bg-gray-300 cursor-not-allowed shadow-none grayscale'
: 'bg-gradient-to-r from-blue-600 to-purple-600'}`}
>
{isLoading ? (
<span className="flex items-center justify-center gap-2">
<i className="fas fa-circle-notch fa-spin"></i> در حال پردازش...
</span>
) : (
'شروع پردازش'
)}
</button>
{/* Enhanced Tips Section - Amber Style (Right Aligned per Screenshot) */}
<div className="relative group overflow-hidden rounded-[2.5rem] p-6 transition-all duration-500 hover:shadow-2xl hover:shadow-amber-200/50 border border-amber-100/40">
{/* Animated Background Layers */}
<div className="absolute inset-0 bg-gradient-to-br from-amber-50/95 via-orange-50/90 to-amber-100/95 backdrop-blur-md -z-10"></div>
<div className="absolute top-0 left-0 w-full h-full bg-gradient-to-r from-transparent via-white/50 to-transparent -translate-x-full animate-[shimmer_4s_infinite] -z-10"></div>
<div className="absolute -bottom-12 -right-12 w-40 h-40 bg-amber-200/20 rounded-full blur-3xl group-hover:bg-amber-300/30 transition-colors duration-1000"></div>
<div className="space-y-6 relative z-10">
{/* Tip 1: Main Instructions (As per screenshot) */}
<div className="flex gap-4 items-start">
{/* Animated Icon Container 1 (Lightbulb) */}
<div className="relative flex-shrink-0">
<div className="absolute inset-0 bg-amber-400 blur-md opacity-30 rounded-full animate-pulse"></div>
<div className="w-10 h-10 bg-white rounded-2xl shadow-sm border border-amber-100 flex items-center justify-center relative transform group-hover:scale-110 transition-transform">
<i className="fas fa-lightbulb text-amber-500 text-lg animate-[bounce_3s_infinite]"></i>
<span className="absolute -top-1 -right-1 flex h-2.5 w-2.5">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-amber-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-2.5 w-2.5 bg-amber-500 border border-white"></span>
</span>
</div>
</div>
<div className="text-right flex-1">
<h5 className="text-amber-950 font-black text-sm mb-2">
راهنمای هوشمند
</h5>
<p className="text-[11.5px] text-amber-900 leading-[1.8] font-medium opacity-95">
در قسمت <span className="font-black text-amber-950 underline decoration-amber-300/60 decoration-4 underline-offset-4">صدای ورودی</span> صدای خودتان و در قسمت <span className="font-black text-amber-950 underline decoration-amber-300/60 decoration-4 underline-offset-4">صدای الگو</span> صدای هدف را بارگذاری کنید؛ هوش مصنوعی صدای شما را دقیقاً به صدای الگو تبدیل می‌کند.
</p>
</div>
</div>
{/* Tip 2: Time Recommendation (Updated Text per user request) */}
<div className="flex gap-4 items-start border-t border-amber-200/40 pt-5">
{/* Animated Icon Container 2 (Stopwatch - Same Size) */}
<div className="relative flex-shrink-0">
<div className="absolute inset-0 bg-amber-400 blur-md opacity-20 rounded-full"></div>
<div className="w-10 h-10 bg-white rounded-2xl shadow-sm border border-amber-100 flex items-center justify-center relative transform group-hover:rotate-12 transition-transform">
<i className="fas fa-stopwatch text-amber-600 text-lg"></i>
<span className="absolute -bottom-1 -left-1 flex h-2 w-2">
<span className="relative inline-flex rounded-full h-2 w-2 bg-orange-400 opacity-60"></span>
</span>
</div>
</div>
<div className="text-right flex-1 pt-1">
<p className="text-[11px] text-amber-950 font-bold leading-relaxed">
نکته: بهترین حالت این است که صدای مدل بین <span className="text-sm font-black text-amber-600 underline decoration-amber-300/60 decoration-4 underline-offset-4 mx-1">۳ تا ۹</span> ثانیه بدون نویز باشه. کیفیت صدای خروجی به کیفیت صدای مدل که اضافه میکنید بستگی داره.
</p>
</div>
</div>
</div>
</div>
</form>
<style>{`
@keyframes shimmer {
100% { transform: translateX(100%); }
}
`}</style>
</div>
);
};
export default CustomVoice;