Tttt / wordpress_export (6).html
Ezmary's picture
Upload wordpress_export (6).html
6cb8d29 verified
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>هوش مصنوعی آلفا صدا | AI Sada</title>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#4A6CFA',
secondary: '#0FD4A8',
accent: '#38b2ac',
dark: '#1A202C',
},
fontFamily: {
vazir: ['Vazirmatn', 'sans-serif'],
}
}
}
}
</script>
<!-- Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@100;300;400;500;600;700;800;900&display=swap" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<!-- React & Babel -->
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<style>
body {
font-family: 'Vazirmatn', sans-serif;
background: #F8F9FC;
color: #1A202C;
overflow-x: hidden;
}
/* Custom Scrollbar */
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-track { background: #f1f1f1; }
::-webkit-scrollbar-thumb { background: #c7c7c7; border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: #a8a8a8; }
.no-scrollbar::-webkit-scrollbar { display: none; }
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
/* Animations */
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
@keyframes scaleIn { from { opacity: 0; transform: scale(0.9); } to { opacity: 1; transform: scale(1); } }
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
@keyframes shimmer { 100% { transform: translateX(100%); } }
.animate-fade-in { animation: fadeIn 0.5s ease-out forwards; }
.animate-scale-in { animation: scaleIn 0.3s ease-out forwards; }
/* Glassmorphism */
.glass-nav {
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.5);
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.1);
}
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel" data-presets="env,react,typescript">
const { useState, useEffect, useRef } = React;
// --- CONFIGURATION ---
const PROXY_URL = '/tts/proxy.php'; // آدرس پروکسی برای امنیت
// --- MODELS DATA ---
const TTS_SPEAKERS = [
{ id: "Charon", name: "شهاب (مرد)", img: "https://uploadkon.ir/uploads/a18705_25IMG-۲۰۲۵۰۷۰۵-۱۱۰۵۴۹.jpg", desc: "صدایی قدرتمند و رسا" },
{ id: "Zephyr", name: "آوا (زن)", img: "https://uploadkon.ir/uploads/029605_25IMG-۲۰۲۵۰۷۰۵-۱۱۱۲۵۲.jpg", desc: "لطیف و دلنشین" },
{ id: "Achird", name: "نوید (مرد)", img: "https://uploadkon.ir/uploads/697e05_25IMG-۲۰۲۵۰۶۰۹-۰۶۴۶۳۷.jpg", desc: "جوان و پرانرژی" },
{ id: "Zubenelgenubi", name: "آرمان (مرد)", img: "https://uploadkon.ir/uploads/a8a705_25IMG-۲۰۲۵۰۷۰۵-۱۱۱۶۲۹.jpg", desc: "گرم و صمیمی" },
{ id: "Vindemiatrix", name: "مهسا (زن)", img: "https://uploadkon.ir/uploads/d74d05_25IMG-۲۰۲۵۰۷۰۵-۱۱۱۸۳۸.jpg", desc: "باوقار و رسمی" },
{ id: "Algieba", name: "آرتین (مرد)", img: "https://uploadkon.ir/uploads/571005_25IMG-۲۰۲۵۰۷۰۵-۱۱۵۰۳۹.jpg", desc: "با اصالت و شیک" }
];
const VC_MODELS = [
{ id: 'shadmehr', name: 'شادمهر عقیلی', img: 'https://app.puzzley.net/uploads/user/Jydo/%D8%AA%D8%BA%DB%8C%D8%B1%20%D8%B5%D8%AF%D8%A7%20%D8%A8%D8%A7%20%D9%87%D9%88%D8%B4%20%D9%85%D8%B5%D9%86%D9%88%D8%B9%DB%8C/1000188203.jpg?_t=1725334498', ref: '/refs/shadmehr.wav' },
{ id: 'moein', name: 'معین', img: 'https://app.puzzley.net/uploads/user/Jydo/%D8%AA%D8%BA%DB%8C%D8%B1%20%D8%B5%D8%AF%D8%A7%20%D8%A8%D8%A7%20%D9%87%D9%88%D8%B4%20%D9%85%D8%B5%D9%86%D9%88%D8%B9%DB%8C/5dbc55de-d6ab-442f-9a00-da874521cc0b.jpg?_t=1725334795', ref: '/refs/moein.wav' },
{ id: 'billie', name: 'بیلی آیلیش', img: 'https://app.puzzley.net/uploads/user/Jydo/%D8%AA%D8%BA%DB%8C%D8%B1%20%D8%B5%D8%AF%D8%A7%20%D8%A8%D8%A7%20%D9%87%D9%88%D8%B4%20%D9%85%D8%B5%D9%86%D9%88%D8%B9%DB%8C/1551c598-f02f-4ced-a037-33d2d7317edd.jpg?_t=1726723022', ref: '/refs/billie.wav' },
{ id: 'chavoshi', name: 'محسن چاوشی', img: 'https://app.puzzley.net/uploads/user/Jydo/%D8%AA%D8%BA%DB%8C%D8%B1%20%D8%B5%D8%AF%D8%A7%20%D8%A8%D8%A7%20%D9%87%D9%88%D8%B4%20%D9%85%D8%B5%D9%86%D9%88%D8%B9%DB%8C/c52eefb1-071e-40ea-9bc2-e20a7c29cb81.jpg?_t=1726907812', ref: '/refs/chavoshi.wav' },
{ id: 'ferdosipour', name: 'عادل فردوسی‌پور', img: 'https://app.puzzley.net/uploads/user/Jydo/%D8%AA%D8%BA%DB%8C%D8%B1%20%D8%B5%D8%AF%D8%A7%20%D8%A8%D8%A7%20%D9%87%D9%88%D8%B4%20%D9%85%D8%B5%D9%86%D9%88%D8%B9%DB%8C/1000188207.jpg?_t=1725334637', ref: '/refs/ferdosipour.wav' },
{ id: 'khiabani', name: 'جواد خیابانی', img: 'https://app.puzzley.net/uploads/user/Jydo/%D8%AA%D8%BA%DB%8C%D8%B1%20%D8%B5%D8%AF%D8%A7%20%D8%A8%D8%A7%20%D9%87%D9%88%D8%B4%20%D9%85%D8%B5%D9%86%D9%88%D8%B9%DB%8C/1000189552.jpg?_t=1726729222', ref: '/refs/khiabani.wav' },
{ id: 'musk', name: 'ایلان ماسک', img: 'https://app.puzzley.net/uploads/user/Jydo/%D9%87%D9%88%D8%B4%20%D9%85%D8%B5%D9%86%D9%88%D8%B9%DB%8C/7ba0101a-83ed-46c3-a2e7-57a85e034b2c.jpg', ref: '/refs/musk.wav' },
{ id: 'maryam', name: 'مریم (اختصاصی آلفا)', img: 'https://app.puzzley.net/uploads/user/Jydo/%D9%87%D9%88%D8%B4%20%D9%85%D8%B5%D9%86%D9%88%D8%B9%DB%8C/ab1e28b106df4c48ac228da6ced9d076.jpeg', ref: '/refs/maryam.wav' }
];
// --- SERVICES ---
const api = {
async getFingerprint() {
return 'fp_' + Math.random().toString(36).substr(2, 9); // Simplified for this view
},
async checkUser(email) {
if (!email) return { status: 'free' };
try {
const res = await fetch('/tts/check_status.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({email})
});
return await res.json();
} catch { return { status: 'free' }; }
},
async sendCode(email) {
return fetch('/tts/send_code.php', {
method: 'POST',
body: JSON.stringify({email})
}).then(r => r.json());
},
async verifyCode(email, code) {
return fetch('/tts/verify_code.php', {
method: 'POST',
body: JSON.stringify({email, code})
}).then(r => r.json());
},
// TTS Generation (Secure Proxy)
async generateTTS(payload) {
const res = await fetch(`${PROXY_URL}?endpoint=generate`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload)
});
if(res.status === 429) throw new Error("محدودیت اعتبار روزانه به پایان رسیده است.");
if(!res.ok) throw new Error("خطا در تولید صدا");
return res.blob();
},
// Voice Conversion Upload (Secure Proxy)
async uploadVoiceConversion(formData) {
const res = await fetch(`${PROXY_URL}?endpoint=vc-upload`, {
method: 'POST',
body: formData
});
if(!res.ok) {
const err = await res.json();
throw new Error(err.message || "خطا در آپلود فایل");
}
return res.json();
},
// Voice Conversion Status (Secure Proxy)
async checkVoiceStatus(jobId) {
const res = await fetch(`${PROXY_URL}?endpoint=vc-status`, {
method: 'POST',
body: JSON.stringify({ job_id: jobId })
});
return res.json();
}
};
// --- COMPONENTS ---
// 1. Navigation (Glass Menu)
const GlassNav = ({ activeTab, onChange }) => {
return (
<div className="sticky top-4 z-50 flex justify-center mb-8 px-4">
<nav className="glass-nav rounded-2xl p-1.5 flex items-center gap-1 shadow-lg max-w-full overflow-x-auto no-scrollbar">
<button
onClick={() => onChange('tts')}
className={`px-4 py-2.5 rounded-xl text-sm font-bold transition-all flex items-center gap-2 whitespace-nowrap
${activeTab === 'tts' ? 'bg-white text-primary shadow-sm' : 'text-gray-500 hover:bg-white/50'}`}
>
<i className="fas fa-keyboard text-lg"></i>
تبدیل متن به صدا
</button>
<button
onClick={() => onChange('vc')}
className={`px-4 py-2.5 rounded-xl text-sm font-bold transition-all flex items-center gap-2 whitespace-nowrap
${activeTab === 'vc' ? 'bg-white text-primary shadow-sm' : 'text-gray-500 hover:bg-white/50'}`}
>
<i className="fas fa-microphone-alt text-lg"></i>
تغییر صدا (AI)
</button>
<button
onClick={() => onChange('podcast')}
className={`px-4 py-2.5 rounded-xl text-sm font-bold transition-all flex items-center gap-2 whitespace-nowrap relative
${activeTab === 'podcast' ? 'bg-white text-primary shadow-sm' : 'text-gray-500 hover:bg-white/50'}`}
>
<i className="fas fa-podcast text-lg"></i>
ساخت پادکست
<span className="absolute -top-1 -left-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"></span>
</span>
<span className="text-[9px] bg-amber-100 text-amber-700 px-1.5 py-0.5 rounded mr-1">بزودی</span>
</button>
</nav>
</div>
);
};
// 2. TTS Component (Text to Speech)
const TTSComponent = ({ user, onLoginRequest }) => {
const [text, setText] = useState('');
const [speaker, setSpeaker] = useState(TTS_SPEAKERS[0]);
const [temp, setTemp] = useState(0.9);
const [isLoading, setIsLoading] = useState(false);
const [audioUrl, setAudioUrl] = useState(null);
const [statusMsg, setStatusMsg] = useState('');
const [isSpeakerModalOpen, setIsSpeakerModalOpen] = useState(false);
const handleGenerate = async () => {
if (!user.email) return onLoginRequest();
if (!text.trim()) return alert("لطفا متن را وارد کنید");
setIsLoading(true);
setAudioUrl(null);
setStatusMsg('');
try {
// Split text logic would go here, simplified for single chunk for robustness in this snippet
const blob = await api.generateTTS({
text,
speaker: speaker.id,
temperature: temp,
email: user.email,
fingerprint: user.fingerprint
});
const url = URL.createObjectURL(blob);
setAudioUrl(url);
} catch (e) {
setStatusMsg(e.message);
} finally {
setIsLoading(false);
}
};
return (
<div className="animate-fade-in bg-white rounded-[24px] shadow-xl border border-gray-100 p-6 md:p-10 relative">
<div className="mb-6">
<label className="block font-bold text-gray-800 mb-2">📝 متن اصلی</label>
<textarea
value={text}
onChange={(e) => setText(e.target.value)}
rows={5}
className="w-full p-4 rounded-xl border border-gray-200 bg-gray-50 focus:bg-white focus:border-primary outline-none transition-all resize-none"
placeholder="متن خود را برای تبدیل به گفتار اینجا وارد کنید..."
></textarea>
<div className="text-left text-xs text-gray-400 mt-2 dir-ltr">{text.length} / 50000</div>
</div>
<div className="mb-6">
<label className="block font-bold text-gray-800 mb-2">🎤 گوینده منتخب</label>
<div
onClick={() => setIsSpeakerModalOpen(true)}
className="flex items-center justify-between p-3 rounded-full border border-gray-200 bg-gray-50 cursor-pointer hover:border-primary transition-all max-w-md mx-auto"
>
<div className="flex items-center gap-3">
<img src={speaker.img} className="w-12 h-12 rounded-full object-cover border-2 border-white shadow-sm" />
<div className="text-right">
<h4 className="font-bold text-gray-800 text-sm">{speaker.name}</h4>
<p className="text-xs text-gray-500">{speaker.desc}</p>
</div>
</div>
<button className="bg-white text-primary text-xs font-bold px-4 py-2 rounded-full shadow-sm border border-gray-100">تغییر</button>
</div>
</div>
<div className="mb-8">
<div className="flex items-center justify-between mb-2">
<label className="font-bold text-gray-800 flex items-center gap-2">
🌡️ خلاقیت و پویایی
<span className="w-5 h-5 rounded-full bg-gray-100 text-gray-500 flex items-center justify-center text-xs cursor-help" title="مقادیر بالاتر صدای متنوع‌تری تولید می‌کنند">!</span>
</label>
<span className="font-bold text-primary bg-blue-50 px-2 py-1 rounded-lg text-sm">{temp}</span>
</div>
<input
type="range" min="0.1" max="1.5" step="0.05" value={temp}
onChange={(e) => setTemp(parseFloat(e.target.value))}
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-primary"
/>
</div>
<button
onClick={handleGenerate}
disabled={isLoading}
className="w-full py-4 rounded-xl font-black text-lg text-white bg-gradient-to-r from-primary to-purple-600 hover:translate-y-[-4px] shadow-lg shadow-primary/30 transition-all flex items-center justify-center gap-3 disabled:opacity-70 disabled:cursor-not-allowed"
>
{isLoading ? <i className="fas fa-circle-notch fa-spin"></i> : <i className="fas fa-wand-magic-sparkles"></i>}
{isLoading ? 'در حال پردازش...' : 'خلق صدا با آلفا'}
</button>
{/* Output Section */}
<div className={`mt-8 p-6 rounded-2xl border-2 border-dashed border-gray-200 bg-gray-50 transition-all ${audioUrl ? 'border-primary bg-white shadow-lg border-solid' : ''}`}>
{!audioUrl && !isLoading && !statusMsg && (
<div className="text-center text-gray-400 font-medium">صدای تولید شده در اینجا ظاهر خواهد شد.</div>
)}
{isLoading && (
<div className="flex flex-col items-center justify-center py-4">
<div className="w-16 h-16 border-4 border-primary/30 border-t-primary rounded-full animate-spin mb-4"></div>
<p className="font-bold text-gray-600 animate-pulse">هوش مصنوعی در حال خواندن متن...</p>
</div>
)}
{statusMsg && (
<div className="text-center text-red-500 bg-red-50 p-3 rounded-xl border border-red-100 font-bold">{statusMsg}</div>
)}
{audioUrl && (
<div className="animate-scale-in">
<p className="text-center text-green-600 font-bold mb-4">✅ صدا با موفقیت تولید شد</p>
<audio controls src={audioUrl} className="w-full mb-4"></audio>
<a href={audioUrl} download="aisada-tts.wav" className="flex items-center justify-center gap-2 w-full py-3 bg-secondary text-white rounded-xl font-bold shadow-lg shadow-teal-500/20 hover:bg-teal-500 transition-colors">
<i className="fas fa-download"></i> دانلود فایل
</a>
</div>
)}
</div>
{/* Speaker Modal */}
{isSpeakerModalOpen && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={() => setIsSpeakerModalOpen(false)}></div>
<div className="relative bg-white rounded-3xl w-full max-w-lg p-6 shadow-2xl max-h-[80vh] overflow-y-auto animate-scale-in">
<div className="flex justify-between items-center mb-6">
<h3 className="text-xl font-black text-gray-800">گالری گویندگان</h3>
<button onClick={() => setIsSpeakerModalOpen(false)} className="text-2xl text-gray-400 hover:text-red-500">&times;</button>
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4">
{TTS_SPEAKERS.map(s => (
<div
key={s.id}
onClick={() => { setSpeaker(s); setIsSpeakerModalOpen(false); }}
className={`cursor-pointer rounded-xl p-2 border-2 transition-all text-center
${speaker.id === s.id ? 'border-primary bg-blue-50' : 'border-transparent hover:bg-gray-50'}`}
>
<img src={s.img} className="w-16 h-16 rounded-full mx-auto mb-2 object-cover border border-gray-200" />
<div className="font-bold text-sm text-gray-800">{s.name}</div>
</div>
))}
</div>
</div>
</div>
)}
</div>
);
};
// 3. Voice Changer Component (Secure Proxy)
const VoiceChangerComponent = ({ user, onLoginRequest }) => {
const [selectedModel, setSelectedModel] = useState(null);
const [file, setFile] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [resultUrl, setResultUrl] = useState(null);
const [status, setStatus] = useState('');
const fileInputRef = useRef(null);
const handleFileChange = (e) => {
if (e.target.files && e.target.files[0]) {
setFile(e.target.files[0]);
setResultUrl(null);
}
};
const handleGenerate = async () => {
if (!user.email) return onLoginRequest();
if (!selectedModel) return alert("لطفا یک مدل را انتخاب کنید");
if (!file) return alert("لطفا فایل صدای خود را آپلود کنید");
setIsLoading(true);
setStatus('در حال ارسال فایل به سرور...');
setResultUrl(null);
try {
// 1. Prepare Data
// Fetch the reference file for the selected model to send to proxy
const refBlob = await fetch(selectedModel.ref).then(r => r.blob());
const formData = new FormData();
formData.append('email', user.email);
formData.append('fingerprint', user.fingerprint);
formData.append('source_audio', file);
formData.append('ref_audio', refBlob, 'ref_audio.wav'); // Proxy needs this
// 2. Upload
const uploadData = await api.uploadVoiceConversion(formData);
const jobId = uploadData.job_id;
// 3. Poll
setStatus('در حال پردازش هوشمند (ممکن است ۱ دقیقه طول بکشد)...');
const poll = setInterval(async () => {
try {
const statusData = await api.checkVoiceStatus(jobId);
if (statusData.status === 'completed') {
clearInterval(poll);
// Link construction (direct HF for speed, or could proxy via PHP too)
setResultUrl(`https://ezmary-sada.hf.space/download/${statusData.filename}`);
setIsLoading(false);
setStatus('');
} else if (statusData.status === 'failed') {
clearInterval(poll);
throw new Error("خطا در پردازش سرور");
}
} catch (e) {
clearInterval(poll);
setIsLoading(false);
setStatus('خطا: ' + e.message);
}
}, 4000);
} catch (e) {
setIsLoading(false);
setStatus('خطا: ' + e.message);
}
};
return (
<div className="animate-fade-in bg-white rounded-[24px] shadow-xl border border-gray-100 p-6 md:p-10 relative">
<div className="text-center mb-8">
<h2 className="text-2xl font-black text-gray-800 mb-2">تغییر صدای جادویی</h2>
<p className="text-gray-500 text-sm">صدای خود را به صدای خوانندگان و مشاهیر تبدیل کنید</p>
</div>
{/* Step 1: Model Selection */}
<div className="mb-8">
<label className="block font-bold text-gray-800 mb-3">۱. انتخاب مدل صدا</label>
<div className="grid grid-cols-3 sm:grid-cols-4 gap-3 max-h-[280px] overflow-y-auto p-1 custom-scrollbar">
{VC_MODELS.map(model => (
<div
key={model.id}
onClick={() => setSelectedModel(model)}
className={`cursor-pointer rounded-xl p-2 border-2 text-center transition-all flex flex-col items-center justify-center
${selectedModel?.id === model.id ? 'border-primary bg-blue-50 shadow-md' : 'border-gray-100 hover:border-gray-300'}`}
>
<img src={model.img} className="w-14 h-14 rounded-full object-cover mb-2 border-2 border-white shadow-sm" />
<span className="text-[10px] font-bold text-gray-700 leading-tight">{model.name}</span>
</div>
))}
</div>
</div>
{/* Step 2: Upload */}
<div className="mb-8">
<label className="block font-bold text-gray-800 mb-3">۲. آپلود صدای شما (ورودی)</label>
<div
onClick={() => fileInputRef.current?.click()}
className={`border-2 border-dashed rounded-2xl p-6 text-center cursor-pointer transition-all bg-gray-50 hover:bg-white hover:border-primary
${file ? 'border-green-400 bg-green-50' : 'border-gray-300'}`}
>
<input type="file" ref={fileInputRef} hidden accept="audio/*" onChange={handleFileChange} />
<div className="text-3xl mb-2">{file ? '🎤' : '📂'}</div>
<p className="text-sm font-bold text-gray-700">{file ? 'فایل انتخاب شد' : 'برای انتخاب فایل کلیک کنید'}</p>
<p className="text-xs text-gray-400 mt-1">{file ? file.name : '(کیفیت بالا، بدون نویز، ۳ تا ۱۰ ثانیه)'}</p>
</div>
</div>
{/* Step 3: Action */}
<button
onClick={handleGenerate}
disabled={isLoading}
className="w-full py-4 rounded-xl font-black text-lg text-white bg-gradient-to-r from-secondary to-teal-600 hover:translate-y-[-4px] shadow-lg shadow-teal-500/30 transition-all flex items-center justify-center gap-3 disabled:opacity-70 disabled:cursor-not-allowed"
>
{isLoading ? <i className="fas fa-circle-notch fa-spin"></i> : <i className="fas fa-microphone-lines"></i>}
{isLoading ? 'در حال جادوگری...' : 'شروع تغییر صدا'}
</button>
{/* Output */}
<div className="mt-8">
{status && (
<div className="text-center text-sm font-bold text-gray-500 animate-pulse mb-4">{status}</div>
)}
{resultUrl && (
<div className="animate-scale-in bg-gray-50 border border-gray-200 rounded-2xl p-6 text-center">
<div className="text-green-600 font-black text-lg mb-4 flex items-center justify-center gap-2">
<i className="fas fa-check-circle"></i> تغییر صدا انجام شد!
</div>
<audio controls src={resultUrl} className="w-full mb-4 shadow-sm rounded-full"></audio>
<a href={resultUrl} download="voice-changed.wav" className="inline-block px-6 py-3 bg-primary text-white rounded-xl font-bold shadow-md hover:bg-blue-600 transition-colors">
دانلود فایل نهایی
</a>
</div>
)}
</div>
{/* FAQ */}
<div className="mt-10 border-t pt-6">
<h3 className="font-bold text-gray-800 mb-4 text-center">سوالات متداول</h3>
<div className="space-y-3">
<div className="bg-gray-50 p-3 rounded-lg text-xs text-gray-600">
<strong>کیفیت خروجی چطور است؟</strong>
<br/>
به کیفیت فایل ورودی شما بستگی دارد. لطفا در محیط ساکت ضبط کنید.
</div>
<div className="bg-gray-50 p-3 rounded-lg text-xs text-gray-600">
<strong>آیا فایلم ذخیره می‌شود؟</strong>
<br/>
خیر، فایل‌ها فقط برای پردازش استفاده شده و سپس حذف می‌شوند.
</div>
</div>
</div>
</div>
);
};
// 4. Podcast Component (Coming Soon)
const PodcastComponent = () => {
return (
<div className="animate-fade-in bg-gradient-to-br from-gray-900 to-gray-800 rounded-[24px] shadow-2xl p-10 text-center text-white relative overflow-hidden">
<div className="absolute top-0 left-0 w-full h-full opacity-20 bg-[url('https://www.transparenttextures.com/patterns/cubes.png')]"></div>
<div className="relative z-10 py-12">
<div className="w-24 h-24 bg-white/10 rounded-full flex items-center justify-center mx-auto mb-6 backdrop-blur-md border border-white/20">
<i className="fas fa-podcast text-4xl text-amber-400"></i>
</div>
<h2 className="text-3xl font-black mb-4">استودیو ساخت پادکست</h2>
<p className="text-gray-300 text-lg mb-8 max-w-md mx-auto leading-relaxed">
به زودی... یک انقلاب در تولید محتوای صوتی.
<br/>
سناریو بدهید، پادکست چند نفره تحویل بگیرید.
</p>
<span className="inline-block px-4 py-2 rounded-full bg-amber-500/20 text-amber-300 border border-amber-500/50 font-bold text-sm">
🚧 در حال توسعه
</span>
</div>
</div>
);
};
// 5. Auth Modal
const AuthModal = ({ isOpen, onClose, onLoginSuccess }) => {
const [step, setStep] = useState(1);
const [email, setEmail] = useState('');
const [code, setCode] = useState('');
const [isLoading, setIsLoading] = useState(false);
if (!isOpen) return null;
const handleSendCode = async () => {
if(!email) return alert("ایمیل را وارد کنید");
setIsLoading(true);
try {
const res = await api.sendCode(email);
if(res.status === 'success') setStep(2);
else alert(res.message);
} catch(e) { alert("خطا در ارتباط"); }
setIsLoading(false);
};
const handleVerify = async () => {
if(!code) return alert("کد را وارد کنید");
setIsLoading(true);
try {
const res = await api.verifyCode(email, code);
if(res.status === 'success') {
localStorage.setItem('userEmail', email);
onLoginSuccess(email);
onClose();
} else alert(res.message);
} catch(e) { alert("خطا در تایید"); }
setIsLoading(false);
};
return (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
<div className="absolute inset-0 bg-black/70 backdrop-blur-sm" onClick={onClose}></div>
<div className="relative bg-white rounded-3xl w-full max-w-sm p-8 shadow-2xl animate-scale-in text-center">
<button onClick={onClose} className="absolute top-4 right-4 text-2xl text-gray-300 hover:text-gray-600">&times;</button>
<h2 className="text-xl font-black text-gray-800 mb-6">ورود / ثبت نام</h2>
{step === 1 ? (
<>
<p className="text-gray-500 text-sm mb-4">برای استفاده از امکانات، ایمیل خود را وارد کنید.</p>
<input
type="email" value={email} onChange={e=>setEmail(e.target.value)}
className="w-full p-3 rounded-xl border border-gray-200 bg-gray-50 mb-4 text-center dir-ltr"
placeholder="example@gmail.com"
/>
<button onClick={handleSendCode} disabled={isLoading} className="w-full py-3 bg-primary text-white rounded-xl font-bold hover:bg-blue-600 transition">
{isLoading ? '...' : 'ارسال کد تایید'}
</button>
</>
) : (
<>
<p className="text-gray-500 text-sm mb-4">کد ۶ رقمی ارسال شده به {email} را وارد کنید.</p>
<input
type="text" value={code} onChange={e=>setCode(e.target.value)}
className="w-full p-3 rounded-xl border border-gray-200 bg-gray-50 mb-4 text-center tracking-[5px] font-bold text-xl"
placeholder="123456"
/>
<button onClick={handleVerify} disabled={isLoading} className="w-full py-3 bg-primary text-white rounded-xl font-bold hover:bg-blue-600 transition">
{isLoading ? '...' : 'تایید و ورود'}
</button>
<button onClick={()=>setStep(1)} className="mt-4 text-xs text-gray-400">تغییر ایمیل</button>
</>
)}
</div>
</div>
);
};
// --- MAIN APP ---
const App = () => {
const [activeTab, setActiveTab] = useState('tts');
const [user, setUser] = useState({ email: null, status: 'free', fingerprint: null });
const [isAuthModalOpen, setIsAuthModalOpen] = useState(false);
useEffect(() => {
const init = async () => {
const fp = await api.getFingerprint();
const storedEmail = localStorage.getItem('userEmail');
if (storedEmail) {
const statusData = await api.checkUser(storedEmail);
setUser({ email: storedEmail, status: statusData.status, fingerprint: fp });
} else {
setUser(prev => ({ ...prev, fingerprint: fp }));
}
};
init();
}, []);
const handleLogout = () => {
localStorage.removeItem('userEmail');
fetch('/tts/logout.php');
window.location.reload();
};
const handleLoginSuccess = async (email) => {
const statusData = await api.checkUser(email);
setUser(prev => ({ ...prev, email, status: statusData.status }));
};
return (
<div className="page-wrapper min-h-screen pb-20">
{/* Header */}
<header className="text-center mb-8 relative z-10 px-4">
<h1 className="text-3xl font-black mb-2 bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">هوش مصنوعی آلفا صدا</h1>
<p className="text-gray-500 text-sm md:text-base">جعبه ابزار صوتی حرفه‌ای: متن به صدا، تغییر صدا و پادکست</p>
<div className="mt-4 flex justify-center">
{user.email ? (
<div className="flex items-center gap-3 bg-white px-4 py-2 rounded-full shadow-sm border border-gray-100">
<span className="font-bold text-xs text-gray-700">{user.email}</span>
<span className={`text-[10px] font-bold px-2 py-0.5 rounded ${user.status === 'paid' ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-500'}`}>
{user.status === 'paid' ? 'اشتراک ویژه' : 'رایگان'}
</span>
<button onClick={handleLogout} className="text-red-500 text-xs hover:font-bold"><i className="fas fa-sign-out-alt"></i></button>
</div>
) : (
<button onClick={() => setIsAuthModalOpen(true)} className="bg-white text-primary px-6 py-2 rounded-full font-bold text-sm shadow-md hover:translate-y-[-2px] transition-transform">
ورود / ثبت نام
</button>
)}
</div>
</header>
{/* Tabs */}
<GlassNav activeTab={activeTab} onChange={setActiveTab} />
{/* Content */}
<div className="container mx-auto px-4 max-w-3xl">
{activeTab === 'tts' && <TTSComponent user={user} onLoginRequest={() => setIsAuthModalOpen(true)} />}
{activeTab === 'vc' && <VoiceChangerComponent user={user} onLoginRequest={() => setIsAuthModalOpen(true)} />}
{activeTab === 'podcast' && <PodcastComponent />}
</div>
<AuthModal
isOpen={isAuthModalOpen}
onClose={() => setIsAuthModalOpen(false)}
onLoginSuccess={handleLoginSuccess}
/>
</div>
);
};
const rootElement = document.getElementById('root');
const root = ReactDOM.createRoot(rootElement);
root.render(<App />);
</script>
</body>
</html>