|
|
<!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> |
|
|
<meta name="description" content="با AI Sada، متن فارسی خود را به صدایی طبیعی و با کیفیت استودیویی تبدیل کنید."> |
|
|
<style> |
|
|
@import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700;800;900&display=swap'); |
|
|
|
|
|
:root { |
|
|
--app-font: 'Vazirmatn', sans-serif; --app-bg: #F8F9FC; --panel-bg: #FFFFFF; |
|
|
--panel-border: #EAEFF7; --input-bg: #F6F8FB; --input-border: #E1E7EF; |
|
|
--text-primary: #1A202C; --text-secondary: #626F86; --text-tertiary: #8A94A6; |
|
|
--accent-primary: #4A6CFA; --accent-primary-hover: #3553D6; --accent-primary-glow: rgba(74, 108, 250, 0.25); |
|
|
--accent-secondary: #0FD4A8; --accent-secondary-hover: #0DA986; --accent-secondary-glow: rgba(15, 212, 168, 0.2); |
|
|
--shadow-sm: 0 1px 2px 0 rgba(26, 32, 44, 0.03); --shadow-md: 0 4px 6px -1px rgba(26, 32, 44, 0.05), 0 2px 4px -2px rgba(26, 32, 44, 0.04); |
|
|
--shadow-lg: 0 10px 15px -3px rgba(26, 32, 44, 0.06), 0 4px 6px -4px rgba(26, 32, 44, 0.05); |
|
|
--shadow-xl: 0 20px 25px -5px rgba(26, 32, 44, 0.07), 0 8px 10px -6px rgba(26, 32, 44, 0.05); |
|
|
--radius-card: 24px; --radius-btn: 14px; --radius-input: 12px; |
|
|
--transition-fast: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); |
|
|
--transition-smooth: all 0.35s cubic-bezier(0.4, 0, 0.2, 1); |
|
|
--glass-bg: rgba(255, 255, 255, 0.75); |
|
|
--glass-border: rgba(255, 255, 255, 0.5); |
|
|
--glass-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.1); |
|
|
} |
|
|
|
|
|
@keyframes fadeIn { from { opacity: 0; transform: translateY(15px); } to { opacity: 1; transform: translateY(0); } } |
|
|
@keyframes modalZoomIn { from { opacity: 0; transform: scale(0.9) translateY(20px); } to { opacity: 1; transform: scale(1) translateY(0); } } |
|
|
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } |
|
|
@keyframes rotate-loader-orbital { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } |
|
|
@keyframes orbit-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } |
|
|
@keyframes satellite-pulse-1 { from { transform: scale(0.7) translateX(-50%); opacity: 0.6; } to { transform: scale(1.1) translateX(-50%); opacity: 1; } } |
|
|
@keyframes satellite-pulse-2 { from { transform: scale(0.7) translateY(-50%); opacity: 0.6; } to { transform: scale(1.1) translateY(-50%); opacity: 1; } } |
|
|
@keyframes satellite-pulse-3 { from { transform: scale(0.7) translateX(50%); opacity: 0.6; } to { transform: scale(1.1) translateX(50%); opacity: 1; } } |
|
|
|
|
|
body { |
|
|
font-family: var(--app-font); direction: rtl; background-color: var(--app-bg); |
|
|
color: var(--text-primary); margin: 0; padding: 0; min-height: 100vh; |
|
|
} |
|
|
.page-wrapper { max-width: 820px; width: 92%; margin: 0 auto; padding: 2.5rem 0; } |
|
|
.app-container { width: 100%; margin: 0 auto; margin-bottom: 5rem; } |
|
|
.app-header { padding: 0.5rem 0 1rem 0; text-align: center; margin-bottom: 1.5rem; animation: fadeIn 0.8s 0.2s ease-out backwards; } |
|
|
.app-header h1 { font-size: 2.5em; font-weight: 900; margin: 0 0 0.8rem 0; background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } |
|
|
.app-header p { font-size: 1.05em; color: var(--text-secondary); margin-top: 0; opacity: 0.9; } |
|
|
|
|
|
/* Glass Navigation */ |
|
|
.glass-nav-container { display: flex; justify-content: center; margin-bottom: 2rem; position: sticky; top: 10px; z-index: 100; } |
|
|
.glass-nav { background: var(--glass-bg); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border: 1px solid var(--glass-border); border-radius: 20px; padding: 0.5rem; display: flex; gap: 0.5rem; box-shadow: var(--glass-shadow); overflow-x: auto; max-width: 100%; white-space: nowrap; } |
|
|
.glass-nav::-webkit-scrollbar { display: none; } |
|
|
.nav-item { background: transparent; border: none; padding: 0.7rem 1.2rem; border-radius: 14px; color: var(--text-secondary); font-family: var(--app-font); font-weight: 700; font-size: 0.95em; cursor: pointer; transition: all 0.3s ease; position: relative; } |
|
|
.nav-item.active { background: #fff; color: var(--accent-primary); box-shadow: 0 4px 12px rgba(0,0,0,0.05); } |
|
|
.nav-item .badge { font-size: 0.6em; background: #FFC107; color: #000; padding: 2px 6px; border-radius: 6px; position: absolute; top: 2px; left: 2px; } |
|
|
|
|
|
/* Tabs Content */ |
|
|
.content-tab { display: none; animation: fadeIn 0.5s ease-out; } |
|
|
.content-tab.active { display: block; } |
|
|
|
|
|
/* Common Styles (From Your Original Code) */ |
|
|
.main-content { padding: 3rem; background-color: var(--panel-bg); border-radius: var(--radius-card); box-shadow: var(--shadow-xl); border: 1px solid var(--panel-border); } |
|
|
.form-group { margin-bottom: 2.2rem; } |
|
|
label { display: block; font-weight: 700; color: var(--text-primary); font-size: 1.1em; margin-bottom: 1rem; } |
|
|
textarea, input[type="text"], input[type="email"] { width: 100%; padding: 1rem 1.2rem; border-radius: var(--radius-input); border: 1px solid var(--input-border); background-color: var(--input-bg); color: var(--text-primary); font-family: var(--app-font); font-size: 1rem; box-sizing: border-box; transition: var(--transition-smooth); } |
|
|
textarea:focus, input:focus { outline: none; border-color: var(--accent-primary); box-shadow: 0 0 0 3px var(--accent-primary-glow); background-color: var(--panel-bg); } |
|
|
.generate-btn { display: flex; align-items: center; justify-content: center; gap: 10px; width: 100%; padding: 1.1rem 1.5rem; font-size: 1.25em; font-weight: 800; background: linear-gradient(95deg, var(--accent-secondary) 0%, var(--accent-primary) 100%); color: #fff; border: none; border-radius: var(--radius-btn); cursor: pointer; transition: all 0.3s ease; box-shadow: 0 6px 12px -3px var(--accent-primary-glow); position: relative; overflow: hidden; } |
|
|
.generate-btn:hover:not(:disabled) { transform: translateY(-5px); box-shadow: 0 8px 20px -4px var(--accent-primary-glow); } |
|
|
.generate-btn:disabled { background: var(--text-tertiary); cursor: not-allowed; } |
|
|
.spinner { width: 20px; height: 20px; border: 3px solid rgba(255, 255, 255, 0.4); border-top-color: #fff; border-radius: 50%; animation: spin 0.8s linear infinite; display: none;} |
|
|
.output-section { margin-top: 3rem; display: flex; align-items: center; justify-content: center; flex-direction: column; min-height: 220px; padding: 2rem; background-color: var(--input-bg); border-radius: var(--radius-card); border: 2px dashed var(--input-border); transition: var(--transition-smooth); } |
|
|
.output-section.has-content { background-color: var(--panel-bg); border: 1px solid var(--panel-border); box-shadow: var(--shadow-lg); padding: 0; min-height: auto; } |
|
|
.status-message { font-weight: 500; color: var(--text-secondary); text-align: center; } |
|
|
.status-message.error { color: #e53e3e; background-color: #fed7d7; padding: 1rem; border-radius: 12px; } |
|
|
|
|
|
/* Specifics for TTS (Your Code) */ |
|
|
#standard-view .char-counter-wrapper { font-size: 0.85em; color: var(--text-tertiary); text-align: left; margin-top: 0.75rem; } |
|
|
#standard-view #char-count { font-weight: 600; color: var(--accent-primary); } |
|
|
#standard-view #selected-speaker-card { display: inline-flex; align-items: center; background: linear-gradient(135deg, var(--input-bg) 0%, var(--panel-bg) 100%); border-radius: 50px; padding: 0.75rem 0.75rem 0.75rem 1.5rem; box-shadow: var(--shadow-md); border: 1px solid var(--panel-border); cursor: pointer; margin-bottom: 1.5rem; width: 100%; max-width: 350px; justify-content: space-between; } |
|
|
#standard-view #selected-speaker-card img { width: 60px; height: 60px; border-radius: 50%; object-fit: cover; margin-left: 1rem; border: 3px solid var(--accent-secondary); } |
|
|
#standard-view #change-speaker-btn { display: inline-flex; padding: 12px 24px; border-radius: 14px; background: var(--panel-bg); border: 1px solid var(--input-border); cursor: pointer; font-weight: 600; } |
|
|
.slider-container { display: flex; align-items: center; gap: 1.5rem; } |
|
|
input[type="range"] { flex-grow: 1; -webkit-appearance: none; width: 100%; height: 6px; background: var(--input-border); border-radius: 3px; outline: none; } |
|
|
input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 24px; height: 24px; background: #fff; border-radius: 50%; cursor: pointer; border: 4px solid var(--accent-primary); margin-top: -9px; box-shadow: var(--shadow-md); } |
|
|
.temperature-value { font-weight: 700; background-color: var(--input-bg); padding: 0.5rem 1rem; border-radius: 8px; border: 1px solid var(--input-border); min-width: 45px; text-align: center; color: var(--accent-primary); } |
|
|
|
|
|
/* New Voice Changer Styles (Matches Your Theme) */ |
|
|
.vc-model-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); gap: 1rem; max-height: 300px; overflow-y: auto; padding: 5px; } |
|
|
.vc-model-item { background: var(--input-bg); border: 2px solid transparent; border-radius: 16px; padding: 0.8rem; text-align: center; cursor: pointer; transition: all 0.2s; display: flex; flex-direction: column; align-items: center; } |
|
|
.vc-model-item:hover { transform: translateY(-3px); } |
|
|
.vc-model-item.selected { border-color: var(--accent-primary); background: #fff; box-shadow: 0 5px 15px rgba(74, 108, 250, 0.2); } |
|
|
.vc-model-item img { width: 60px; height: 60px; border-radius: 50%; object-fit: cover; margin-bottom: 0.5rem; border: 2px solid #fff; box-shadow: var(--shadow-sm); } |
|
|
.vc-model-item span { font-size: 0.85em; font-weight: 700; color: var(--text-primary); } |
|
|
|
|
|
.upload-area-vc { border: 2px dashed var(--input-border); border-radius: var(--radius-card); padding: 2rem; text-align: center; cursor: pointer; background: var(--input-bg); transition: all 0.3s; } |
|
|
.upload-area-vc:hover { border-color: var(--accent-primary); background: #fff; } |
|
|
.file-status-bar { display: none; align-items: center; justify-content: space-between; background: #e6fffa; border: 1px solid #b2f5ea; padding: 0.8rem 1rem; border-radius: 12px; margin-top: 1rem; color: #047481; font-weight: 600; } |
|
|
|
|
|
/* Podcast Section */ |
|
|
.coming-soon-wrapper { text-align: center; padding: 4rem 2rem; background: linear-gradient(135deg, #1A202C, #2D3748); border-radius: var(--radius-card); color: white; position: relative; overflow: hidden; } |
|
|
.coming-soon-wrapper::before { content: ''; position: absolute; top: -50%; left: -50%; width: 200%; height: 200%; background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 60%); animation: spin 15s linear infinite; } |
|
|
|
|
|
/* Headers & Auth */ |
|
|
.header-actions { margin-top: 1rem; text-align: center; } |
|
|
#user-status-container { padding: 0.75rem 1.5rem; background-color: var(--input-bg); border-radius: var(--radius-btn); display: none; align-items: center; justify-content: center; gap: 0.5rem; border: 1px solid var(--panel-border); max-width: 320px; margin: 0 auto; } |
|
|
#user-status-container .user-sub-status.status-paid { background-color: var(--accent-secondary-glow); color: var(--accent-secondary-hover); } |
|
|
#login-check-btn { background: var(--input-bg); border: 1px solid var(--panel-border); color: var(--text-primary); font-weight: 600; padding: 0.75rem 1.5rem; border-radius: var(--radius-btn); cursor: pointer; } |
|
|
#login-check-btn:hover { background: var(--accent-primary); color: white; } |
|
|
|
|
|
/* Loader */ |
|
|
.orbital-loader { width: 110px; height: 110px; position: relative; animation: rotate-loader-orbital 10s linear infinite; } |
|
|
.orbit { position: absolute; top: 50%; left: 50%; border: 2px dashed rgba(74, 108, 250, 0.35); border-radius: 50%; } |
|
|
.orbit:nth-child(1) { width: 35px; height: 35px; margin: -17.5px 0 0 -17.5px; animation: orbit-spin 2.8s linear infinite reverse; } |
|
|
.orbit:nth-child(2) { width: 65px; height: 65px; margin: -32.5px 0 0 -32.5px; animation: orbit-spin 3.8s linear infinite; } |
|
|
.orbit:nth-child(3) { width: 95px; height: 95px; margin: -47.5px 0 0 -47.5px; animation: orbit-spin 4.8s linear infinite reverse; } |
|
|
.satellite { position: absolute; width: 10px; height: 10px; border-radius: 50%; background-color: var(--accent-primary); } |
|
|
.orbit:nth-child(1) .satellite { top: -5px; left: 50%; } |
|
|
|
|
|
/* Audio Player */ |
|
|
.simple-player-container { width: 100%; display: flex; flex-direction: column; gap: 1rem; align-items: center; } |
|
|
.play-controls { display: flex; gap: 1rem; width: 100%; align-items: center; } |
|
|
.play-pause-btn-simple { background: linear-gradient(135deg, var(--accent-secondary), var(--accent-primary)); color: white; width: 50px; height: 50px; border-radius: 50%; border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; } |
|
|
.audio-download-btn-new { display: inline-flex; align-items: center; justify-content: center; gap: 10px; width: 100%; padding: 0.9rem; background: linear-gradient(95deg, var(--accent-secondary), var(--accent-primary)); color: white; border-radius: 14px; text-decoration: none; font-weight: 700; margin-top: 1rem; } |
|
|
|
|
|
/* Modals */ |
|
|
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.6); backdrop-filter: blur(5px); display: none; align-items: center; justify-content: center; z-index: 1000; } |
|
|
.modal-overlay.visible { display: flex; } |
|
|
.modal-dialog { background: #fff; padding: 2rem; border-radius: 24px; width: 90%; max-width: 500px; position: relative; } |
|
|
.close-modal-btn { position: absolute; top: 1rem; left: 1rem; font-size: 2rem; background: none; border: none; cursor: pointer; line-height: 1; } |
|
|
|
|
|
@media(max-width: 600px) { .vc-model-grid { grid-template-columns: repeat(3, 1fr); } } |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
|
|
|
<div class="page-wrapper"> |
|
|
<div class="app-container"> |
|
|
|
|
|
|
|
|
<header class="app-header"> |
|
|
<h1>هوش مصنوعی آلفا صدا</h1> |
|
|
<p>کیفیت استودیو، قدرت هوش مصنوعی. متن فارسی را به صدایی فراتر از انتظار تبدیل کنید.</p> |
|
|
<div class="header-actions"> |
|
|
<div id="user-status-container"> |
|
|
<span id="user-email-display" class="user-email"></span> |
|
|
<div id="user-status-details"> |
|
|
<span id="user-sub-status-display" class="user-sub-status"></span> |
|
|
<button id="logout-btn" class="logout-btn">خروج</button> |
|
|
</div> |
|
|
</div> |
|
|
<button id="login-check-btn">ورود / ثبت نام</button> |
|
|
</div> |
|
|
</header> |
|
|
|
|
|
|
|
|
<div class="glass-nav-container"> |
|
|
<nav class="glass-nav"> |
|
|
<button class="nav-item active" onclick="switchTab('tts')">تبدیل متن به صدا</button> |
|
|
<button class="nav-item" onclick="switchTab('vc')">تغییر صدا (AI)</button> |
|
|
<button class="nav-item" onclick="switchTab('podcast')">ساخت پادکست <span class="badge">بزودی</span></button> |
|
|
</nav> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div id="standard-view" class="content-tab active"> |
|
|
<main class="main-content"> |
|
|
<form id="standard-tts-form" onsubmit="return false;"> |
|
|
<div class="form-group"><label for="text-input-standard">📝 متن اصلی</label><textarea id="text-input-standard" rows="5" placeholder="متن خود را برای تبدیل به گفتار اینجا وارد کنید..."></textarea><div class="char-counter-wrapper"><span id="char-count">0</span> / <span id="char-max">50000</span> نویسه</div></div> |
|
|
<div class="form-group"><label for="prompt-input-standard">🗣️ توصیف لحن و احساس (اختیاری)</label><input type="text" id="prompt-input-standard" placeholder="مثال: با لحنی آرام و قصهگو"></div> |
|
|
<div class="form-group"> |
|
|
<label>🎤 گوینده منتخب</label> |
|
|
<div id="selected-speaker-display"> |
|
|
<div id="selected-speaker-card" title="برای تغییر گوینده کلیک کنید"> |
|
|
<img id="selected-speaker-img" src="" alt="عکس گوینده"> |
|
|
<div id="selected-speaker-info"> |
|
|
<h3 id="selected-speaker-name"></h3> |
|
|
<p id="selected-speaker-desc"></p> |
|
|
</div> |
|
|
</div> |
|
|
<button type="button" id="change-speaker-btn">تغییر گوینده</button> |
|
|
</div> |
|
|
</div> |
|
|
<div class="form-group"><div class="label-with-info"><label for="temperature-slider-standard">🌡️ خلاقیت و پویایی صدا</label><div class="info-icon" id="temp-info-icon" role="button" tabindex="0" aria-label="اطلاعات بیشتر">!</div></div><div class="slider-container"><input type="range" id="temperature-slider-standard" class="temperature-slider" min="0.1" max="1.5" step="0.05" value="0.9"><span id="temperature-value-standard" class="temperature-value">0.9</span></div></div> |
|
|
<p id="credit-status-message"></p> |
|
|
<button type="submit" id="generate-btn-standard" class="generate-btn"><span class="btn-text">✨ خلق صدا با آلفا</span><div class="spinner"></div></button> |
|
|
</form> |
|
|
<div id="output-section-standard" class="output-section"> |
|
|
<div id="status-message-standard" class="status-message">صدای تولید شده در اینجا ظاهر خواهد شد.</div> |
|
|
<div id="loading-animation-wrapper-standard" class="loading-animation-wrapper"><div class="orbital-loader"><div class="orbit"><div class="satellite"></div></div><div class="orbit"><div class="satellite"></div></div><div class="orbit"><div class="satellite"></div></div></div><p class="loading-text">در حال پردازش هوشمند و تولید صدا...</p></div> |
|
|
<div id="audio-player-content-standard" class="audio-player-content"></div> |
|
|
</div> |
|
|
</main> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div id="voice-clone-view-tab" class="content-tab"> |
|
|
<main class="main-content"> |
|
|
<div style="text-align:center; margin-bottom:2rem;"> |
|
|
<h2 style="font-size:1.5em;margin-bottom:0.5rem;color:var(--text-primary);">تغییر صدای جادویی</h2> |
|
|
<p style="color:var(--text-secondary);">صدای خود را به صدای خوانندگان و مشاهیر تبدیل کنید.</p> |
|
|
</div> |
|
|
|
|
|
<div class="form-group"> |
|
|
<label>۱. انتخاب مدل صدا:</label> |
|
|
<div class="vc-model-grid" id="vc-models-container"> |
|
|
|
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="form-group"> |
|
|
<label>۲. آپلود صدای خودتان:</label> |
|
|
<div class="upload-area-vc" onclick="document.getElementById('vc-file-input').click()"> |
|
|
<div style="font-size:2.5rem;margin-bottom:1rem;color:var(--accent-primary);">🎤</div> |
|
|
<p>برای انتخاب فایل کلیک کنید (۳ تا ۹ ثانیه)</p> |
|
|
<input type="file" id="vc-file-input" accept="audio/*" style="display:none;" onchange="handleVcFile(this)"> |
|
|
</div> |
|
|
<div id="vc-file-status" class="file-status-bar"> |
|
|
<span id="vc-filename-display"></span> |
|
|
<span>✅ آماده</span> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<button id="vc-generate-btn" class="generate-btn" onclick="startVoiceConversion()"> |
|
|
<span class="btn-text">شروع تغییر صدا</span> |
|
|
<div class="spinner"></div> |
|
|
</button> |
|
|
|
|
|
<div id="vc-output-section" class="output-section"> |
|
|
<div id="vc-status-message" class="status-message">فایل خروجی اینجا نمایش داده میشود.</div> |
|
|
<div id="vc-loader" style="display:none; flex-direction:column; align-items:center; gap:1rem;"> |
|
|
<div class="orbital-loader"><div class="orbit"><div class="satellite"></div></div><div class="orbit"><div class="satellite"></div></div></div> |
|
|
<p style="font-weight:700; color:var(--accent-primary);">در حال پردازش در سرور ابری...</p> |
|
|
</div> |
|
|
<div id="vc-result-container" style="width:100%; display:none; text-align:center;"> |
|
|
<p style="color:#047481; font-weight:700; margin-bottom:1rem;">تغییر صدا با موفقیت انجام شد!</p> |
|
|
<audio id="vc-audio-player" controls style="width:100%; margin-bottom:1rem;"></audio> |
|
|
<a id="vc-download-btn" href="#" class="audio-download-btn-new" download>دانلود فایل نهایی</a> |
|
|
</div> |
|
|
</div> |
|
|
</main> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div id="podcast-view" class="content-tab"> |
|
|
<div class="coming-soon-wrapper"> |
|
|
<h2>استودیو ساخت پادکست</h2> |
|
|
<p>به زودی... سناریو بدهید، پادکست چند نفره تحویل بگیرید.</p> |
|
|
<div style="font-size:4rem; margin-top:2rem;">🎙️</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<audio id="hidden-audio-player" style="display: none;"></audio> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div id="speaker-modal" class="modal-overlay"><div class="modal-dialog"><div class="modal-header"><h2>گالری گویندگان</h2><button type="button" class="close-modal-btn" data-modal-id="speaker-modal">×</button></div><div id="speaker-grid"></div></div></div> |
|
|
<div id="info-modal" class="modal-overlay"><div class="modal-dialog"><div class="modal-header"><h2>راهنما</h2><button class="close-modal-btn" data-modal-id="info-modal">×</button></div><p>این پارامتر خلاقیت را تنظیم میکند.</p></div></div> |
|
|
|
|
|
<div id="email-modal" class="modal-overlay"> |
|
|
<div class="modal-dialog" style="max-width: 400px; text-align:center;"> |
|
|
<div class="modal-header"><h2 id="email-modal-title">ورود / ثبت نام</h2><button class="close-modal-btn" data-modal-id="email-modal">×</button></div> |
|
|
<form id="email-form" onsubmit="return false;"> |
|
|
<input type="email" id="login-email-input" required placeholder="example@gmail.com" style="width:100%; padding:1rem; border-radius:12px; border:1px solid #ddd; margin-bottom:1rem;"> |
|
|
<button type="submit" id="send-code-btn" class="generate-btn">ارسال کد تایید</button> |
|
|
</form> |
|
|
<form id="code-form" onsubmit="return false;" style="display: none;"> |
|
|
<input type="text" id="code-input" required placeholder="123456" style="width:100%; padding:1rem; border-radius:12px; border:1px solid #ddd; margin-bottom:1rem; text-align:center; letter-spacing:5px;"> |
|
|
<button type="submit" id="verify-code-btn" class="generate-btn">تایید و ورود</button> |
|
|
<button type="button" id="back-to-email-btn" style="background:none; border:none; margin-top:10px; color:#888;">بازگشت</button> |
|
|
</form> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<input type="hidden" id="selected_speaker_id_storage" value="Charon"> |
|
|
|
|
|
</div> |
|
|
|
|
|
<script> |
|
|
document.addEventListener('DOMContentLoaded', () => { |
|
|
|
|
|
|
|
|
const PROXY_URL = '/tts/proxy.php'; |
|
|
let currentUser = { email: null, status: 'free', fingerprint: null }; |
|
|
let selectedVcModel = null; |
|
|
|
|
|
|
|
|
window.switchTab = (tabId) => { |
|
|
document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active')); |
|
|
document.querySelectorAll('.content-tab').forEach(el => el.classList.remove('active')); |
|
|
|
|
|
const btns = document.querySelectorAll('.nav-item'); |
|
|
if(tabId === 'tts') { btns[0].classList.add('active'); document.getElementById('standard-view').classList.add('active'); } |
|
|
if(tabId === 'vc') { btns[1].classList.add('active'); document.getElementById('voice-clone-view-tab').classList.add('active'); initVcModels(); } |
|
|
if(tabId === 'podcast') { btns[2].classList.add('active'); document.getElementById('podcast-view').classList.add('active'); } |
|
|
}; |
|
|
|
|
|
|
|
|
const generationButtons = document.querySelectorAll('#generate-btn-standard'); |
|
|
const creditStatusMessage = document.getElementById('credit-status-message'); |
|
|
const userStatusContainer = document.getElementById('user-status-container'); |
|
|
const userEmailDisplay = document.getElementById('user-email-display'); |
|
|
const userSubStatusDisplay = document.getElementById('user-sub-status-display'); |
|
|
const loginCheckBtn = document.getElementById('login-check-btn'); |
|
|
const emailModal = document.getElementById('email-modal'); |
|
|
const speakerModal = document.getElementById('speaker-modal'); |
|
|
const infoModal = document.getElementById('info-modal'); |
|
|
const speakerGridInModal = document.getElementById('speaker-grid'); |
|
|
const selectedSpeakerIdStorage = document.getElementById('selected_speaker_id_storage'); |
|
|
const selectedSpeakerImg = document.getElementById('selected-speaker-img'); |
|
|
const selectedSpeakerName = document.getElementById('selected-speaker-name'); |
|
|
const selectedSpeakerDesc = document.getElementById('selected-speaker-desc'); |
|
|
const mainAudioPlayer = document.getElementById('hidden-audio-player'); |
|
|
|
|
|
const speakers = [ { id: "Charon", name: "شهاب (مرد)", desc: "صدایی قدرمند و رسا", imgUrl: "https://uploadkon.ir/uploads/a18705_25IMG-۲۰۲۵۰۷۰۵-۱۱۰۵۴۹.jpg" }, { id: "Zephyr", name: "آوا (زن)", desc: "لطیف و دلنشین", imgUrl: "https://uploadkon.ir/uploads/029605_25IMG-۲۰۲۵۰۷۰۵-۱۱۱۲۵۲.jpg" }, { id: "Achird", name: "نوید (مرد)", desc: "جوان و پرانرژی", imgUrl: "https://uploadkon.ir/uploads/697e05_25IMG-۲۰۲۵۰۶۰۹-۰۶۴۶۳۷.jpg" }, { id: "Zubenelgenubi", name: "آرمان (مرد)", desc: "گرم و صمیمی", imgUrl: "https://uploadkon.ir/uploads/a8a705_25IMG-۲۰۲۵۰۷۰۵-۱۱۱۶۲۹.jpg" }, { id: "Vindemiatrix", name: "مهسا (زن)", desc: "باوقار و رسمی", imgUrl: "https://uploadkon.ir/uploads/d74d05_25IMG-۲۰۲۵۰۷۰۵-۱۱۱۸۳۸.jpg" } ]; |
|
|
|
|
|
|
|
|
const vcModels = [ |
|
|
{ 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: 'https://uploadkon.ir/uploads/55c918_25شادمهر-قوی-2-.mp3' }, |
|
|
{ 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: 'https://uploadkon.ir/uploads/f8bb17_25معین-2-.mp3' }, |
|
|
{ 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: 'https://uploadkon.ir/uploads/c21018_25بیلی-آیلیش-2-.mp3' }, |
|
|
{ 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: 'https://uploadkon.ir/uploads/7ca518_25محسن-چاووشی-3-2-.mp3' } |
|
|
]; |
|
|
|
|
|
|
|
|
async function getBrowserFingerprint() { return 'fp_' + Math.floor(Math.random() * 10000000).toString(16); } |
|
|
function splitTextIntoChunks(text, maxChunkLength = 2500) { const chunks = []; let remainingText = text.trim(); if (remainingText.length <= maxChunkLength) return [remainingText]; while (remainingText.length > 0) { if (remainingText.length <= maxChunkLength) { chunks.push(remainingText); break; } let chunkCandidate = remainingText.substring(0, maxChunkLength); let splitIndex = -1; const delimiters = ['\n', '.', '؟', '!', '؛', '،', ' ']; for (const delimiter of delimiters) { const lastIndex = chunkCandidate.lastIndexOf(delimiter); if (lastIndex !== -1) { splitIndex = lastIndex + 1; break; } } if (splitIndex === -1) splitIndex = maxChunkLength; chunks.push(remainingText.substring(0, splitIndex).trim()); remainingText = remainingText.substring(splitIndex).trim(); } return chunks.filter(chunk => chunk.length > 0); } |
|
|
async function mergeAudioBlobs(blobs) { if (!blobs || blobs.length === 0) return null; if (blobs.length === 1) return blobs[0]; const audioContext = new (window.AudioContext || window.webkitAudioContext)(); const decodedBuffers = await Promise.all(blobs.map(blob => blob.arrayBuffer().then(buffer => audioContext.decodeAudioData(buffer)))); const totalLength = decodedBuffers.reduce((acc, buffer) => acc + buffer.length, 0); const firstBuffer = decodedBuffers[0]; const finalBuffer = audioContext.createBuffer(firstBuffer.numberOfChannels, totalLength, firstBuffer.sampleRate); let offset = 0; for (const buffer of decodedBuffers) { for (let channel = 0; channel < buffer.numberOfChannels; channel++) { finalBuffer.copyToChannel(buffer.getChannelData(channel), channel, offset); } offset += buffer.length; } return bufferToWave(finalBuffer); } |
|
|
function bufferToWave(abuffer) { const numOfChan = abuffer.numberOfChannels, length = abuffer.length * numOfChan * 2 + 44, buffer = new ArrayBuffer(length), view = new DataView(buffer), channels = []; let i, sample, offset = 0, pos = 0; const setUint16 = (data) => { view.setUint16(pos, data, true); pos += 2; }; const setUint32 = (data) => { view.setUint32(pos, data, true); pos += 4; }; setUint32(0x46464952); setUint32(length - 8); setUint32(0x45564157); setUint32(0x20746d66); setUint32(16); setUint16(1); setUint16(numOfChan); setUint32(abuffer.sampleRate); setUint32(abuffer.sampleRate * 2 * numOfChan); setUint16(numOfChan * 2); setUint16(16); setUint32(0x61746164); setUint32(length - pos - 4); for (i = 0; i < abuffer.numberOfChannels; i++) { channels.push(abuffer.getChannelData(i)); } while (pos < length) { for (i = 0; i < numOfChan; i++) { sample = Math.max(-1, Math.min(1, channels[i][offset])); sample = (0.5 + sample < 0 ? sample * 32768 : sample * 32767) | 0; view.setInt16(pos, sample, true); pos += 2; } offset++; } return new Blob([view], { type: 'audio/wav' }); } |
|
|
const formatTime = (s) => { if (isNaN(s) || s < 0) return '0:00'; const m = Math.floor(s / 60); return `${m}:${Math.floor(s % 60).toString().padStart(2, '0')}`; }; |
|
|
|
|
|
|
|
|
async function checkUserStatus(email) { if (!email) { updateUIForUserState({ status: 'free', email: null }); return; } try { const response = await fetch('/tts/check_status.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: email }) }); const result = await response.json(); updateUIForUserState({ ...result, email: email }); } catch (error) { console.error('Error checking user status:', error); updateUIForUserState({ status: 'free', email: email }); } } |
|
|
function updateUIForUserState(userData) { currentUser = { ...currentUser, ...userData }; const isLoggedIn = !!currentUser.email; if (isLoggedIn) { localStorage.setItem('userEmail', currentUser.email); userEmailDisplay.textContent = currentUser.email; loginCheckBtn.style.display = 'none'; userStatusContainer.style.display = 'inline-flex'; if (currentUser.status === 'paid') { userSubStatusDisplay.textContent = `اشتراک ویژه تا ${currentUser.expires_at}`; userSubStatusDisplay.className = 'user-sub-status status-paid'; } else { userSubStatusDisplay.textContent = 'کاربر رایگان'; userSubStatusDisplay.className = 'user-sub-status status-free'; } } else { localStorage.removeItem('userEmail'); currentUser.email = null; userStatusContainer.style.display = 'none'; loginCheckBtn.style.display = 'inline-block'; } } |
|
|
|
|
|
|
|
|
const updateSelectedSpeakerDisplay = (speakerId) => { const speaker = speakers.find(s => s.id === speakerId) || speakers[0]; selectedSpeakerImg.src = speaker.imgUrl; selectedSpeakerName.textContent = speaker.name; selectedSpeakerDesc.textContent = speaker.desc; selectedSpeakerIdStorage.value = speaker.id; }; |
|
|
const createSpeakerCardsInModal = () => { speakerGridInModal.innerHTML = ''; speakers.forEach((speaker) => { const card = document.createElement('label'); card.className = 'speaker-card'; card.innerHTML = `<input type="radio" name="modal_speaker_selection" value="${speaker.id}" ${speaker.id === selectedSpeakerIdStorage.value ? 'checked' : ''}><div class="speaker-visual"><img src="${speaker.imgUrl}" alt="${speaker.name}" loading="lazy"></div><div class="speaker-name">${speaker.name}</div>`; card.addEventListener('click', () => { updateSelectedSpeakerDisplay(speaker.id); document.getElementById('speaker-modal').classList.remove('visible'); }); speakerGridInModal.appendChild(card); }); }; |
|
|
|
|
|
function createPlayerInstance(containerId) { const playerContent = document.getElementById(containerId); if (!playerContent) return; playerContent.innerHTML = `<div class="simple-player-container"><div class="play-controls"><button type="button" class="play-pause-btn-simple" aria-label="Play/Pause"><svg viewBox="0 0 24 24" class="play-icon"><path d="M8 5v14l11-7z"></path></svg><svg viewBox="0 0 24 24" class="pause-icon" style="display:none;"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"></path></svg></button><div class="progress-wrapper"><span class="time-display current-time">0:00</span><input type="range" class="audio-progress-bar" value="0" min="0" max="100" step="0.1"><span class="time-display total-time">0:00</span></div></div><a href="#" class="audio-download-btn-new" download="ai_sada_output.wav" title="دانلود فایل صوتی"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19.35 10.04A7.49 7.49 0 0 0 12 4C9.11 4 6.6 5.64 5.35 8.04A5.994 5.994 0 0 0 0 14a6 6 0 0 0 6 6h13a5 5 0 0 0 5-5c0-2.64-2.05-4.78-4.65-4.96zM17 13l-5 5-5-5h3V9h4v4h3z"></path></svg><span>دانلود فایل صوتی</span></a></div>`; const playPauseBtn = playerContent.querySelector('.play-pause-btn-simple'), progressBar = playerContent.querySelector('.audio-progress-bar'); playPauseBtn.addEventListener('click', () => { mainAudioPlayer.paused ? mainAudioPlayer.play() : mainAudioPlayer.pause(); }); progressBar.addEventListener('input', () => { if (!isNaN(mainAudioPlayer.duration)) mainAudioPlayer.currentTime = (progressBar.value / 100) * mainAudioPlayer.duration; }); } |
|
|
const updatePlayerUI = () => { const isPlaying = !(mainAudioPlayer.paused || mainAudioPlayer.ended), { currentTime, duration } = mainAudioPlayer; document.querySelectorAll('.simple-player-container').forEach(player => { const playIcon = player.querySelector('.play-icon'), pauseIcon = player.querySelector('.pause-icon'), currentTimeEl = player.querySelector('.current-time'), totalTimeEl = player.querySelector('.total-time'), progressBar = player.querySelector('.audio-progress-bar'); if (playIcon) playIcon.style.display = isPlaying ? 'none' : 'block'; if (pauseIcon) pauseIcon.style.display = isPlaying ? 'block' : 'none'; if (currentTimeEl) currentTimeEl.textContent = formatTime(currentTime); if (totalTimeEl) totalTimeEl.textContent = isFinite(duration) ? formatTime(duration) : '0:00'; if (progressBar) progressBar.value = isFinite(duration) && duration > 0 ? (currentTime / duration) * 100 : 0; }); }; |
|
|
|
|
|
|
|
|
(function() { |
|
|
const form = document.getElementById('standard-tts-form'); if (!form) return; |
|
|
const textInput = form.querySelector('#text-input-standard'), promptInput = form.querySelector('#prompt-input-standard'), tempSlider = form.querySelector('#temperature-slider-standard'), tempValueSpan = form.querySelector('#temperature-value-standard'), generateBtn = form.querySelector('#generate-btn-standard'), btnText = generateBtn.querySelector('.btn-text'), btnSpinner = generateBtn.querySelector('.spinner'), outputSection = document.getElementById('output-section-standard'), statusMessage = outputSection.querySelector('#status-message-standard'), loadingAnimation = outputSection.querySelector('#loading-animation-wrapper-standard'), playerContent = outputSection.querySelector('#audio-player-content-standard'), charCount = form.querySelector('#char-count'), charMax = form.querySelector('#char-max'), MAX_CHARS = 50000; |
|
|
charMax.textContent = MAX_CHARS.toLocaleString('fa-IR'); |
|
|
textInput.addEventListener('input', () => { const len = textInput.value.length; charCount.textContent = len.toLocaleString('fa-IR'); charCount.style.color = len > MAX_CHARS ? 'red' : 'var(--accent-primary)'; }); |
|
|
tempSlider.addEventListener('input', () => tempValueSpan.textContent = tempSlider.value); |
|
|
const showLoadingState = () => { generateBtn.disabled = true; btnSpinner.style.display = 'inline-block'; btnText.textContent = 'در حال پردازش...'; playerContent.style.display = 'none'; outputSection.classList.remove('has-content'); statusMessage.style.display = 'none'; loadingAnimation.style.display = 'flex'; mainAudioPlayer.src = ''; }; |
|
|
const showResultState = (isSuccess, msg = '') => { loadingAnimation.style.display = 'none'; if (isSuccess) { playerContent.style.display = 'block'; outputSection.classList.add('has-content'); } else { statusMessage.innerHTML = msg || 'خطا.'; statusMessage.style.display = 'block'; statusMessage.classList.add('error'); } generateBtn.disabled = false; btnSpinner.style.display = 'none'; btnText.textContent = '✨ خلق صدا با آلفا'; }; |
|
|
form.addEventListener('submit', async () => { |
|
|
if (generateBtn.disabled) return; |
|
|
if (!currentUser.email) { alert('برای تولید صدا، لطفا ابتدا وارد حساب کاربری خود شوید.'); loginCheckBtn.click(); return; } |
|
|
if (!textInput.value.trim()) { showResultState(false, '<b>خطا:</b> متن ورودی خالی است.'); return; } |
|
|
showLoadingState(); |
|
|
const textChunks = splitTextIntoChunks(textInput.value), allAudioBlobs = []; let hasError = false; |
|
|
for (let i = 0; i < textChunks.length; i++) { |
|
|
btnText.textContent = `در حال پردازش بخش ${i + 1} از ${textChunks.length}...`; |
|
|
try { |
|
|
const response = await fetch('/tts/proxy.php?endpoint=generate', { |
|
|
method: 'POST', |
|
|
headers: {'Content-Type': 'application/json'}, |
|
|
body: JSON.stringify({ text: textChunks[i], prompt: promptInput.value, speaker: selectedSpeakerIdStorage.value, temperature: parseFloat(tempSlider.value), email: currentUser.email, fingerprint: currentUser.fingerprint }) |
|
|
}); |
|
|
if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.message || `خطای سرور (${response.status})`); } |
|
|
const blob = await response.blob(); |
|
|
allAudioBlobs.push(blob); |
|
|
} catch (error) { showResultState(false, `<b>خطا:</b> ${error.message}`); hasError = true; break; } |
|
|
} |
|
|
if (!hasError && allAudioBlobs.length > 0) { |
|
|
try { |
|
|
const finalBlob = await mergeAudioBlobs(allAudioBlobs); |
|
|
if (finalBlob) { |
|
|
const audioUrl = URL.createObjectURL(finalBlob); |
|
|
mainAudioPlayer.src = audioUrl; |
|
|
const downloadLink = playerContent.querySelector('.audio-download-btn-new'); |
|
|
if (downloadLink) downloadLink.href = audioUrl; |
|
|
showResultState(true); |
|
|
} else throw new Error("ادغام ناموفق"); |
|
|
} catch (error) { showResultState(false, `<b>خطا:</b> ${error.message}`); } |
|
|
} |
|
|
}); |
|
|
createPlayerInstance('audio-player-content-standard'); |
|
|
if (statusMessage) statusMessage.style.display = 'block'; |
|
|
})(); |
|
|
|
|
|
|
|
|
function initVcModels() { |
|
|
const grid = document.getElementById('vc-models-container'); |
|
|
if(!grid || grid.children.length > 0) return; |
|
|
vcModels.forEach(m => { |
|
|
const div = document.createElement('div'); |
|
|
div.className = 'vc-model-item'; |
|
|
div.onclick = () => { document.querySelectorAll('.vc-model-item').forEach(i=>i.classList.remove('selected')); div.classList.add('selected'); selectedVcModel = m; }; |
|
|
div.innerHTML = `<img src="${m.img}"><span>${m.name}</span>`; |
|
|
grid.appendChild(div); |
|
|
}); |
|
|
} |
|
|
|
|
|
window.handleVcFile = (input) => { |
|
|
if(input.files && input.files[0]) { |
|
|
document.getElementById('vc-filename-display').textContent = input.files[0].name; |
|
|
document.getElementById('vc-file-status').style.display = 'flex'; |
|
|
} |
|
|
}; |
|
|
|
|
|
window.startVoiceConversion = async () => { |
|
|
if(!currentUser.email) { document.getElementById('email-modal').classList.add('visible'); return; } |
|
|
if(!selectedVcModel) return alert('مدل را انتخاب کنید'); |
|
|
const fileInput = document.getElementById('vc-file-input'); |
|
|
if(!fileInput.files[0]) return alert('فایل را آپلود کنید'); |
|
|
|
|
|
const btn = document.getElementById('vc-generate-btn'); |
|
|
const loader = document.getElementById('vc-loader'); |
|
|
const resultDiv = document.getElementById('vc-result-container'); |
|
|
const statusMsg = document.getElementById('vc-status-message'); |
|
|
const outputSec = document.getElementById('vc-output-section'); |
|
|
|
|
|
btn.disabled = true; |
|
|
btn.querySelector('.spinner').style.display = 'inline-block'; |
|
|
loader.style.display = 'flex'; |
|
|
statusMsg.style.display = 'none'; |
|
|
resultDiv.style.display = 'none'; |
|
|
outputSec.classList.remove('has-content'); |
|
|
|
|
|
try { |
|
|
|
|
|
const refBlob = await fetch(selectedVcModel.ref).then(r=>r.blob()); |
|
|
const formData = new FormData(); |
|
|
formData.append('email', currentUser.email); |
|
|
formData.append('fingerprint', currentUser.fingerprint); |
|
|
formData.append('source_audio', fileInput.files[0]); |
|
|
formData.append('ref_audio', refBlob, 'ref.wav'); |
|
|
|
|
|
const upRes = await fetch('/tts/proxy.php?endpoint=vc-upload', { method: 'POST', body: formData }); |
|
|
if(!upRes.ok) throw new Error('خطا در آپلود'); |
|
|
const { job_id } = await upRes.json(); |
|
|
|
|
|
const poll = setInterval(async () => { |
|
|
try { |
|
|
const stRes = await fetch('/tts/proxy.php?endpoint=vc-status', { method: 'POST', body: JSON.stringify({job_id}) }); |
|
|
const stData = await stRes.json(); |
|
|
if(stData.status === 'completed') { |
|
|
clearInterval(poll); |
|
|
const url = `https://ezmary-sada.hf.space/download/${stData.filename}`; |
|
|
document.getElementById('vc-audio-player').src = url; |
|
|
document.getElementById('vc-download-btn').href = url; |
|
|
loader.style.display = 'none'; |
|
|
resultDiv.style.display = 'block'; |
|
|
outputSec.classList.add('has-content'); |
|
|
btn.disabled = false; |
|
|
btn.querySelector('.spinner').style.display = 'none'; |
|
|
} else if(stData.status === 'failed') { |
|
|
throw new Error('خطا در پردازش'); |
|
|
} |
|
|
} catch(e) { clearInterval(poll); alert(e.message); loader.style.display='none'; statusMsg.style.display='block'; btn.disabled=false; } |
|
|
}, 3000); |
|
|
} catch(e) { alert(e.message); loader.style.display='none'; statusMsg.style.display='block'; btn.disabled=false; } |
|
|
}; |
|
|
|
|
|
|
|
|
async function initializeApp() { |
|
|
currentUser.fingerprint = await getBrowserFingerprint(); |
|
|
setupEventListeners(); |
|
|
const storedEmail = localStorage.getItem('userEmail'); |
|
|
await checkUserStatus(storedEmail); |
|
|
updateSelectedSpeakerDisplay(selectedSpeakerIdStorage.value || speakers[0].id); |
|
|
} |
|
|
|
|
|
function setupEventListeners() { |
|
|
loginCheckBtn.addEventListener('click', () => { document.getElementById('email-modal').classList.add('visible'); }); |
|
|
document.getElementById('logout-btn').addEventListener('click', () => { localStorage.removeItem('userEmail'); location.reload(); }); |
|
|
document.querySelectorAll('.close-modal-btn').forEach(b => b.addEventListener('click', () => b.closest('.modal-overlay').classList.remove('visible'))); |
|
|
['timeupdate', 'play', 'pause'].forEach(e => mainAudioPlayer.addEventListener(e, updatePlayerUI)); |
|
|
document.getElementById('selected-speaker-card').addEventListener('click', () => { createSpeakerCardsInModal(); document.getElementById('speaker-modal').classList.add('visible'); }); |
|
|
document.getElementById('change-speaker-btn').addEventListener('click', (e) => { e.stopPropagation(); createSpeakerCardsInModal(); document.getElementById('speaker-modal').classList.add('visible'); }); |
|
|
document.getElementById('temp-info-icon').addEventListener('click', () => document.getElementById('info-modal').classList.add('visible')); |
|
|
|
|
|
|
|
|
const emailForm = document.getElementById('email-form'); |
|
|
const codeForm = document.getElementById('code-form'); |
|
|
emailForm.addEventListener('submit', async (e) => { |
|
|
e.preventDefault(); |
|
|
const email = document.getElementById('login-email-input').value; |
|
|
const btn = document.getElementById('send-code-btn'); |
|
|
btn.disabled = true; btn.textContent = '...'; |
|
|
try { |
|
|
const res = await fetch('/tts/send_code.php', { method: 'POST', body: JSON.stringify({email}) }); |
|
|
const d = await res.json(); |
|
|
if(d.status === 'success') { emailForm.style.display='none'; codeForm.style.display='block'; } |
|
|
else alert(d.message); |
|
|
} catch(e) { alert('خطا'); } |
|
|
btn.disabled = false; btn.textContent = 'ارسال کد تایید'; |
|
|
}); |
|
|
codeForm.addEventListener('submit', async (e) => { |
|
|
e.preventDefault(); |
|
|
const email = document.getElementById('login-email-input').value; |
|
|
const code = document.getElementById('code-input').value; |
|
|
const btn = document.getElementById('verify-code-btn'); |
|
|
btn.disabled = true; btn.textContent = '...'; |
|
|
try { |
|
|
const res = await fetch('/tts/verify_code.php', { method: 'POST', body: JSON.stringify({email, code}) }); |
|
|
const d = await res.json(); |
|
|
if(d.status === 'success') { |
|
|
localStorage.setItem('userEmail', email); |
|
|
checkUserStatus(email); |
|
|
document.getElementById('email-modal').classList.remove('visible'); |
|
|
} else alert(d.message); |
|
|
} catch(e) { alert('خطا'); } |
|
|
btn.disabled = false; btn.textContent = 'تایید'; |
|
|
}); |
|
|
document.getElementById('back-to-email-btn').addEventListener('click', () => { codeForm.style.display='none'; emailForm.style.display='block'; }); |
|
|
} |
|
|
|
|
|
initializeApp(); |
|
|
}); |
|
|
</script> |
|
|
|
|
|
</body> |
|
|
</html> |