Spaces:
Running
Running
| 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; | |