| <!DOCTYPE html> |
| <html lang="fa" dir="rtl"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> |
| <title>Alpha Ai | استودیو تقویت صدا</title> |
| |
| <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> |
| <link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@100;300;400;500;700;900&display=swap" rel="stylesheet"> |
|
|
| <style> |
| :root { |
| --primary-color: #4f46e5; |
| --secondary-color: #08c295; |
| --premium-color: #FFC107; |
| --premium-glow: rgba(255, 193, 7, 0.3); |
| --border-color: #e2e8f0; |
| --text-dark: #1e293b; |
| --text-secondary: #626F86; |
| --text-muted: #64748b; |
| --bg-glass: rgba(255, 255, 255, 0.95); |
| } |
| |
| body { |
| font-family: 'Vazirmatn', sans-serif; |
| background: #f1f5f9; |
| background-image: radial-gradient(#cbd5e1 1px, transparent 1px); |
| background-size: 20px 20px; |
| min-height: 100vh; |
| color: var(--text-dark); |
| padding-bottom: 60px; |
| } |
| |
| |
| .header-box { text-align: center; padding: 2.5rem 1rem; } |
| .header-title { font-weight: 900; letter-spacing: -1px; margin-bottom: 0.5rem; color: #0f172a; } |
| .title-gradient { |
| background: linear-gradient(135deg, #4f46e5 0%, #08c295 100%); |
| -webkit-background-clip: text; -webkit-text-fill-color: transparent; |
| background-clip: text; text-fill-color: transparent; |
| } |
| .header-desc { font-weight: 400; color: #475569; font-size: 1rem; max-width: 600px; margin: 0 auto; line-height: 1.8; } |
| |
| |
| #subscription-status-badge { |
| display: inline-block; padding: 6px 16px; border-radius: 20px; |
| font-size: 0.9em; font-weight: 700; margin-top: 1.5rem; |
| letter-spacing: 0.5px; text-shadow: 0 1px 2px rgba(0,0,0,0.1); |
| animation: badge-fade-in 0.6s 0.5s ease-out backwards; display: none; |
| } |
| @keyframes badge-fade-in { from { opacity: 0; transform: translateY(-10px) scale(0.95); } to { opacity: 1; transform: translateY(0) scale(1); } } |
| .free-badge { background: linear-gradient(45deg, #6c757d, #495057); color: white; box-shadow: 0 4px 10px rgba(108, 117, 125, 0.3); } |
| .paid-badge { background: linear-gradient(45deg, var(--premium-color), #ffca2c); color: #333; box-shadow: 0 4px 10px var(--premium-glow); } |
| |
| |
| @keyframes slideUpFadeIn { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } |
| .usage-info-container { margin-top: 1rem; } |
| .usage-info-line { opacity: 0; animation: slideUpFadeIn 0.7s ease-out forwards; } |
| .usage-info-container .usage-info-line:nth-child(1) { animation-delay: 0.7s; } |
| .usage-info-container .usage-info-line:nth-child(2) { animation-delay: 0.9s; } |
| .usage-info-free { font-size: 0.9em; font-weight: 600; color: var(--text-secondary); margin-bottom: 0.5rem; } |
| .usage-info-premium { font-size: 0.95em; font-weight: 800; background: linear-gradient(45deg, var(--premium-color), #ffca2c); -webkit-background-clip: text; -webkit-text-fill-color: transparent; margin: 0; text-shadow: 0 1px 3px rgba(255, 193, 7, 0.2); } |
| |
| |
| .glass-card { |
| background: var(--bg-glass); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); |
| border-radius: 24px; border: 1px solid white; box-shadow: 0 20px 40px -10px rgba(0,0,0,0.08); |
| padding: 1.5rem; margin-bottom: 1.5rem; |
| } |
| |
| |
| .upload-area { border: 2px dashed #cbd5e1; border-radius: 20px; padding: 2.5rem 1rem; text-align: center; cursor: pointer; transition: all 0.3s ease; background: #f8fafc; margin-bottom: 20px; position: relative; } |
| .upload-area:hover, .upload-area.active { border-color: var(--primary-color); background: #fff; box-shadow: 0 10px 30px -10px rgba(79, 70, 229, 0.1); transform: translateY(-2px); } |
| .icon-box { width: 65px; height: 65px; background: #eff6ff; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 15px; color: var(--primary-color); font-size: 1.8rem; position: relative; transition: transform 0.3s ease; } |
| .upload-area.file-selected .icon-box:hover { transform: scale(1.1); } |
| .icon-box i { position: absolute; top: 50%; left: 50%; transition: opacity 0.4s ease, transform 0.4s ease; } |
| .icon-mic { opacity: 1; transform: translate(-50%, -50%) scale(1); } |
| .icon-play, .icon-pause { opacity: 0; transform: translate(-50%, -50%) scale(0); } |
| .upload-area.file-selected .icon-mic { opacity: 0; transform: translate(-50%, -50%) scale(0); } |
| .upload-area.file-selected .icon-play { opacity: 1; transform: translate(-50%, -50%) scale(1); } |
| .upload-area.file-selected.is-playing .icon-play { opacity: 0; transform: translate(-50%, -50%) scale(0); } |
| .upload-area.file-selected.is-playing .icon-pause { opacity: 1; transform: translate(-50%, -50%) scale(1); } |
| |
| |
| .solver-grid { display: grid; grid-template-columns: 1fr; gap: 10px; margin-bottom: 20px; } .solver-card { position: relative; cursor: pointer; } .solver-card input { display: none; } .solver-content { display: flex; align-items: center; padding: 12px 15px; border: 2px solid var(--border-color); border-radius: 16px; background: white; transition: all 0.3s ease; } .solver-icon { width: 40px; height: 40px; background: #f1f5f9; border-radius: 10px; display: flex; align-items: center; justify-content: center; color: #64748b; font-size: 1.1rem; margin-left: 12px; transition: 0.3s; } .solver-text h6 { margin: 0; font-weight: 700; font-size: 0.9rem; color: var(--text-dark); } .solver-text p { margin: 0; font-size: 0.75rem; color: var(--text-muted); margin-top: 2px; } .check-circle { width: 20px; height: 20px; border-radius: 50%; border: 2px solid #cbd5e1; margin-right: auto; transition: 0.3s; display: flex; align-items: center; justify-content: center; } .check-circle::after { content: '\f00c'; font-family: "Font Awesome 6 Free"; font-weight: 900; font-size: 10px; color: white; opacity: 0; transform: scale(0); transition: 0.2s; } .solver-card input:checked + .solver-content { border-color: var(--primary-color); background: #f5f3ff; } .solver-card input:checked + .solver-content .solver-icon { background: var(--primary-color); color: white; } .solver-card input:checked + .solver-content .check-circle { background: var(--primary-color); border-color: var(--primary-color); } .solver-card input:checked + .solver-content .check-circle::after { opacity: 1; transform: scale(1); } .denoise-card { cursor: pointer; display: block; margin-bottom: 20px; } .denoise-card input { display: none; } .denoise-inner { display: flex; align-items: center; justify-content: space-between; padding: 12px 15px; border: 2px solid var(--border-color); border-radius: 50px; background: white; transition: 0.3s; } .denoise-left { display: flex; align-items: center; gap: 10px; } .denoise-ico { width: 35px; height: 35px; border-radius: 50%; background: #f1f5f9; color: #64748b; display: flex; align-items: center; justify-content: center; transition: 0.4s; } .denoise-toggle { width: 40px; height: 22px; background: #cbd5e1; border-radius: 20px; position: relative; transition: 0.3s; } .denoise-toggle::after { content: ''; position: absolute; width: 16px; height: 16px; background: white; border-radius: 50%; top: 3px; left: 3px; transform: translateX(0); transition: 0.3s; } .denoise-card input:checked + .denoise-inner { border-color: #10b981; background: #ecfdfdf5; } .denoise-card input:checked + .denoise-inner .denoise-ico { background: #10b981; color: white; transform: rotate(360deg); } .denoise-card input:checked + .denoise-inner .denoise-toggle { background: #10b981; } .denoise-card input:checked + .denoise-inner .denoise-toggle::after { transform: translateX(18px); } .slider-group { background: #f8fafc; border-radius: 16px; padding: 15px; margin-bottom: 15px; } .slider-label { display: flex; justify-content: space-between; font-weight: 700; font-size: 0.85rem; color: #334155; margin-bottom: 8px; } .slider-desc { font-size: 0.75rem; color: #64748b; margin-top: 8px; line-height: 1.5; background: rgba(255,255,255,0.5); padding: 6px; border-radius: 6px; } input[type=range] { -webkit-appearance: none; width: 100%; background: transparent; } input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; height: 20px; width: 20px; border-radius: 50%; background: var(--primary-color); cursor: pointer; margin-top: -8px; box-shadow: 0 2px 6px rgba(0,0,0,0.2); } input[type=range]::-webkit-slider-runnable-track { width: 100%; height: 4px; cursor: pointer; background: #cbd5e1; border-radius: 10px; } |
| |
| |
| .btn-magic { background: linear-gradient(135deg, #4f46e5 0%, #6366f1 100%); color: white; border: none; padding: 15px; border-radius: 16px; font-weight: 800; width: 100%; font-size: 1.1rem; box-shadow: 0 10px 25px -5px rgba(79, 70, 229, 0.4); transition: 0.3s; } .btn-magic:hover { transform: translateY(-2px); box-shadow: 0 15px 35px -5px rgba(79, 70, 229, 0.5); } .btn-magic:disabled { filter: grayscale(1); opacity: 0.7; cursor: not-allowed; } |
| .history-card { background: white; border-radius: 20px; border: 1px solid #e2e8f0; overflow: hidden; transition: all 0.3s ease; box-shadow: 0 5px 20px -5px rgba(0,0,0,0.05); } .history-card-header { padding: 1.25rem; } .history-card-title { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; } .history-card-title h6 { font-weight: 800; margin: 0; color: var(--text-dark); } .request-badge { font-size: 0.7rem; font-weight: 700; background: #f1f5f9; color: #64748b; padding: 5px 10px; border-radius: 8px; } .status-description { background: #f8fafc; border: 1px solid #e2e8f0; color: #475569; font-size: 0.85rem; padding: 1rem; border-radius: 12px; margin-bottom: 1.25rem; } .progress-display { text-align: center; margin-bottom: 1rem; } .progress-percentage { font-size: 2.5rem; font-weight: 900; color: var(--primary-color); letter-spacing: -1px; } .progress-bar-custom { height: 10px; background: #e2e8f0; border-radius: 10px; overflow: hidden; width: 100%; } .progress-bar-custom-fill { height: 100%; width: 0%; border-radius: 10px; background: linear-gradient(90deg, var(--secondary-color), var(--primary-color)); transition: width 0.5s ease; } .action-buttons { display: flex; gap: 10px; } .btn-delete-icon { width: 50px; flex-shrink: 0; background: #fee2e2; color: #ef4444; border: none; border-radius: 12px; font-size: 1rem; transition: 0.3s; } .btn-delete-icon:hover { background: #ef4444; color: white; } .btn-status { width: 100%; padding: 10px; font-weight: 700; border-radius: 12px; border: none; font-size: 0.9rem; transition: 0.3s; } .history-card.processing .btn-status { background: #f1f5f9; color: #475569; } .history-card.completed .btn-status { background: var(--secondary-color); color: white; cursor: pointer; } .history-card.failed .btn-status { background: #fecaca; color: #b91c1c; } .history-card.completed .progress-percentage { color: var(--secondary-color); font-size: 1.8rem; } .history-card.failed .progress-percentage { color: #ef4444; } |
| .history-card-body { max-height: 0; overflow: hidden; transition: max-height 0.5s ease-out; background: #f8fafc; border-top: 1px solid transparent; padding: 0 1.25rem; } .history-card.expanded { border-color: var(--primary-color); } .history-card.expanded .history-card-body { max-height: 500px; border-top-color: #e2e8f0; padding: 1.25rem; overflow-y: auto; } @keyframes slideInFadeUp { from { opacity: 0; transform: translateY(15px); } to { opacity: 1; transform: translateY(0); } } .audio-player-wrapper { opacity: 0; animation: slideInFadeUp 0.5s forwards; } .history-card.expanded .audio-player-wrapper:nth-child(2) { animation-delay: 0.1s; } |
| .history-separator { border: 0; height: 1px; background: linear-gradient(to right, transparent, #cbd5e1, transparent); margin: 2rem 0; } |
| |
| |
| .modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); backdrop-filter: blur(4px); z-index: 9999; display: none; justify-content: center; align-items: center; padding: 20px; } @keyframes pop { from{transform:scale(0.9)} to{transform:scale(1)} } .modal-box { background: white; padding: 25px; border-radius: 20px; width: 100%; max-width: 320px; text-align: center; box-shadow: 0 20px 40px rgba(0,0,0,0.2); animation: pop 0.3s; } |
| @keyframes pulse-premium { 0% { box-shadow: 0 0 0 0 rgba(255, 193, 7, 0.7); } 70% { box-shadow: 0 0 0 15px rgba(255, 193, 7, 0); } 100% { box-shadow: 0 0 0 0 rgba(255, 193, 7, 0); } } .limit-modal-icon { width: 70px; height: 70px; background: linear-gradient(45deg, var(--premium-color), #ffda77); border-radius: 50%; color: #fff; display: flex; align-items: center; justify-content: center; font-size: 2rem; margin: 0 auto 1.5rem; animation: pulse-premium 2s infinite; } .btn-upgrade { background: linear-gradient(45deg, var(--premium-color), #ffca2c); color: #333; font-weight: 800; border: none; } |
| |
| @keyframes fadeInSubtle { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } } |
| .queue-status-message { |
| font-size: 0.8rem; font-weight: 600; color: var(--text-muted); |
| margin-top: 0.75rem; opacity: 0; max-height: 0; |
| overflow: hidden; transition: opacity 0.5s ease, max-height 0.5s ease; |
| } |
| .queue-status-message.visible { opacity: 1; max-height: 50px; animation: fadeInSubtle 0.5s ease-out; } |
| .queue-status-message i { margin-left: 5px; } |
| |
| .mini-player { width: 100%; height: 40px; } |
| </style> |
| </head> |
| <body> |
|
|
| |
| <div id="deleteModal" class="modal-overlay"> |
| <div class="modal-box"> |
| <i class="fas fa-trash-alt fa-3x text-danger mb-3"></i> |
| <h5 class="fw-bold">حذف فایل؟</h5> |
| <div class="d-flex gap-2 mt-4"> |
| <button class="btn btn-light w-50 fw-bold rounded-pill" onclick="closeModal()">لغو</button> |
| <button class="btn btn-danger w-50 fw-bold rounded-pill" id="confirmDeleteBtn">حذف</button> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="timeLimitModal" class="modal-overlay"> |
| <div class="modal-box"> |
| <div class="limit-modal-icon"><i class="fas fa-star"></i></div> |
| <h5 class="fw-bold">ارتقا برای فایلهای طولانی</h5> |
| <p class="text-muted small my-3">در نسخه رایگان، امکان پردازش فایلهای صوتی تا ۶۰ ثانیه وجود دارد. برای حذف این محدودیت و استفاده نامحدود، اشتراک خود را ارتقا دهید.</p> |
| <div class="d-flex flex-column gap-2 mt-4"> |
| <button class="btn btn-upgrade w-100 fw-bold rounded-pill" id="upgradeFromModalBtn">⭐️ ارتقا به نسخه کامل</button> |
| <button class="btn btn-light w-100 fw-bold rounded-pill" onclick="closeTimeLimitModal()">متوجه شدم</button> |
| </div> |
| </div> |
| </div> |
|
|
| <div class="container"> |
| <div class="header-box"> |
| <h1 class="header-title display-6"><span class="title-gradient">Alpha Ai</span></h1> |
| <p class="header-desc">کیفیت صدای ضبطشده خود را با یک کلیک به سطح استودیویی برسانید. نویز پسزمینه را حذف کنید و وضوح گفتار را به سادگی افزایش دهید.</p> |
| <div id="subscription-status-badge"></div> |
| <div class="usage-info-container"> |
| <p class="usage-info-line usage-info-free">تقویت صدا برای کاربران رایگان تا ۶۰ ثانیه</p> |
| <p class="usage-info-line usage-info-premium">برای کاربران دارای اشتراک بدون محدودیت زمانی</p> |
| </div> |
| </div> |
|
|
| <div class="row g-4 justify-content-center"> |
| |
| <div class="col-12 col-lg-6"> |
| <div class="glass-card"> |
| <form id="uploadForm"> |
| <div class="upload-area" id="dropZone" onclick="document.getElementById('srcFile').click()"> |
| <div class="icon-box"> |
| <i class="fas fa-microphone-alt icon-mic"></i> |
| <i class="fas fa-play icon-play"></i> |
| <i class="fas fa-pause icon-pause"></i> |
| </div> |
| <h6 class="fw-bold mb-1">انتخاب فایل صوتی</h6> |
| <small class="text-muted" id="fileNameText">کلیک کنید (MP3, WAV)</small> |
| <input type="file" id="srcFile" hidden accept="audio/*" onchange="handleFile(this)"> |
| </div> |
| |
| <div class="mt-4"> |
| <div class="d-flex align-items-center justify-content-between mb-3 p-3 rounded-4" data-bs-toggle="collapse" data-bs-target="#settingsCollapse" style="cursor:pointer; background: #fff; border: 1px solid #e2e8f0; box-shadow: 0 2px 5px rgba(0,0,0,0.02);"> |
| <span class="fw-bold text-dark"><i class="fas fa-sliders-h me-2 text-primary"></i> تنظیمات کیفیت</span> |
| <i class="fas fa-chevron-down text-muted small"></i> |
| </div> |
| <div class="collapse" id="settingsCollapse"> |
| <h6 class="fw-bold text-secondary mb-2 small px-2">الگوریتم پردازش (Solver)</h6> |
| <div class="solver-grid"> |
| <label class="solver-card"> |
| <input type="radio" name="solver" value="Midpoint" checked> |
| <div class="solver-content"> |
| <div class="solver-icon"><i class="fas fa-balance-scale"></i></div> |
| <div class="solver-text flex-grow-1 mx-2"> |
| <h6>Midpoint (پیشنهادی)</h6> |
| <p>بهترین تعادل بین سرعت و کیفیت.</p> |
| </div> |
| <div class="check-circle"></div> |
| </div> |
| </label> |
| <label class="solver-card"> |
| <input type="radio" name="solver" value="RK4"> |
| <div class="solver-content"> |
| <div class="solver-icon"><i class="fas fa-microscope"></i></div> |
| <div class="solver-text flex-grow-1 mx-2"> |
| <h6>RK4 (دقیق)</h6> |
| <p>حداکثر دقت ریاضی، کمی کندتر.</p> |
| </div> |
| <div class="check-circle"></div> |
| </div> |
| </label> |
| <label class="solver-card"> |
| <input type="radio" name="solver" value="Euler"> |
| <div class="solver-content"> |
| <div class="solver-icon"><i class="fas fa-bolt"></i></div> |
| <div class="solver-text flex-grow-1 mx-2"> |
| <h6>Euler (سریع)</h6> |
| <p>پردازش سریع برای تست اولیه.</p> |
| </div> |
| <div class="check-circle"></div> |
| </div> |
| </label> |
| </div> |
|
|
| <h6 class="fw-bold text-secondary mb-2 small px-2 mt-3">حذف نویز</h6> |
| <label class="denoise-card"> |
| <input type="checkbox" id="denoising"> |
| <div class="denoise-inner"> |
| <div class="denoise-left"> |
| <div class="denoise-ico"><i class="fas fa-magic"></i></div> |
| <div> |
| <div class="fw-bold text-dark small">حذف نویز قبل از بهبود کیفیت</div> |
| <div class="text-muted" style="font-size: 0.7rem;">اگر صدای شما دارای نویز پسزمینه زیادی است، تیک بزنید</div> |
| </div> |
| </div> |
| <div class="denoise-toggle"></div> |
| </div> |
| </label> |
|
|
| <div class="slider-group"> |
| <div class="slider-label"> |
| <span>عمق پردازش (NFE)</span> |
| <span class="text-primary" id="nfeVal">64</span> |
| </div> |
| <input type="range" min="1" max="128" step="1" value="64" id="nfe" oninput="document.getElementById('nfeVal').innerText=this.value"> |
| <div class="slider-desc">عدد بالاتر: کیفیت نهایی بهتر.<br>عدد پایین: سرعت پردازش بالاتر.</div> |
| </div> |
|
|
| <div class="slider-group"> |
| <div class="slider-label"> |
| <span>شفافیت و دما (Tau)</span> |
| <span class="text-primary" id="tauVal">0.5</span> |
| </div> |
| <input type="range" min="0" max="1" step="0.01" value="0.5" id="tau" oninput="document.getElementById('tauVal').innerText=this.value"> |
| <div class="slider-desc">عدد کمتر: صدای طبیعیتر.<br>عدد بیشتر: صدای شفافتر و کریستالی.</div> |
| </div> |
| </div> |
| </div> |
| |
| <button type="submit" class="btn-magic" id="submitBtn">شروع فرآیند بهبود صدا</button> |
| </form> |
| </div> |
| </div> |
|
|
| |
| <div class="col-12 col-lg-5"> |
| <div class="glass-card"> |
| <div class="d-flex justify-content-between align-items-center mb-3"> |
| <h6 class="fw-bold m-0"><i class="fas fa-history text-secondary me-2"></i>سوابق پردازش</h6> |
| <button onclick="openDeleteModal('ALL')" class="btn btn-sm text-danger bg-danger-subtle rounded-pill px-3 fw-bold">پاکسازی همه</button> |
| </div> |
| <div id="historyList"></div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <audio id="previewAudio" hidden></audio> |
|
|
| <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> |
| <script> |
| const STORAGE_KEY = 'sada_resemble_embedded_v2'; |
| const DB_NAME = 'SadaAudioDB_v2'; |
| const DB_VERSION = 1; |
| const STORE_NAME = 'original_audio'; |
| const PREMIUM_PAGE_ID = '1149636'; |
| const PREMIUM_URL = '#/nav/online/news/getSingle/1149636/eyJpdiI6InZSVUdlLzBlR0FzOHZJdXFZeWhER0E9PSIsInZhbHVlIjoiWFhqRXBLc29vSFpHdk9nYmRjZGVuWHRHRHVSZHRlTG1BUENLaE5mNXBNVVRGWFg3ZWN0djJ5K1dIY1RqTHJGaCIsIm1hYyI6IjIzYzFlZTMwYmVmMTdkYjQ0YTQ4YWMxNmFhN2RmNWQ2OTU0MjI0ZWVlZGI3ZjJjMjhkNmQxNjM4MDFlZTIxNmUiLCJ0YWciOiIifQ==/20934991'; |
| |
| let db; |
| let userSubscriptionStatus = 'free'; |
| |
| const uploadArea = document.getElementById('dropZone'); |
| const iconBox = uploadArea.querySelector('.icon-box'); |
| const previewAudio = document.getElementById('previewAudio'); |
| let previewAudioBlobUrl = null; |
| |
| |
| function initDB() { |
| const request = indexedDB.open(DB_NAME, DB_VERSION); |
| request.onerror = (event) => console.error("Database error:", event.target.errorCode); |
| request.onsuccess = (event) => { |
| db = event.target.result; |
| cleanupOldFiles(); |
| renderHistory(); |
| resumeActivePolls(); |
| }; |
| request.onupgradeneeded = (event) => { |
| let db = event.target.result; |
| db.createObjectStore(STORE_NAME, { keyPath: 'id' }); |
| }; |
| } |
| |
| function saveFileToDB(id, blob) { if (!db) return; const transaction = db.transaction([STORE_NAME], 'readwrite'); const store = transaction.objectStore(STORE_NAME); store.put({ id: id, blob: blob, timestamp: new Date().getTime() }); } |
| function getFileFromDB(id, callback) { if (!db) return callback(null); const transaction = db.transaction([STORE_NAME]); const store = transaction.objectStore(STORE_NAME); const request = store.get(id); request.onsuccess = (event) => { callback(event.target.result ? event.target.result.blob : null); }; request.onerror = () => callback(null); } |
| function deleteFileFromDB(id) { if (!db) return; const transaction = db.transaction([STORE_NAME], 'readwrite'); const store = transaction.objectStore(STORE_NAME); store.delete(id); } |
| function clearDB() { if (!db) return; const transaction = db.transaction([STORE_NAME], 'readwrite'); transaction.objectStore(STORE_NAME).clear(); } |
| function cleanupOldFiles() { if (!db) return; const transaction = db.transaction([STORE_NAME], 'readwrite'); const store = transaction.objectStore(STORE_NAME); const sevenDaysAgo = new Date().getTime() - (7 * 24 * 60 * 60 * 1000); store.openCursor().onsuccess = (event) => { const cursor = event.target.result; if (cursor) { if (cursor.value.timestamp < sevenDaysAgo) { cursor.delete(); } cursor.continue(); } }; } |
| |
| const getJobs = () => JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}'); |
| const saveJob = (data) => { const jobs = getJobs(); jobs[data.job_id] = data; localStorage.setItem(STORAGE_KEY, JSON.stringify(jobs)); }; |
| const getAudioDuration = (file) => new Promise((resolve) => { const audio = document.createElement('audio'); audio.onloadedmetadata = () => { resolve(audio.duration); URL.revokeObjectURL(audio.src); }; audio.src = URL.createObjectURL(file); }); |
| |
| |
| async function handleFile(input) { |
| const file = input.files[0]; |
| if (file) { |
| const duration = await getAudioDuration(file); |
| if (userSubscriptionStatus === 'free' && duration > 60.5) { |
| document.getElementById('srcFile').value = ''; |
| document.getElementById('timeLimitModal').style.display = 'flex'; |
| return; |
| } |
| document.getElementById('fileNameText').innerText = file.name; |
| if (previewAudioBlobUrl) URL.revokeObjectURL(previewAudioBlobUrl); |
| previewAudioBlobUrl = URL.createObjectURL(file); |
| previewAudio.src = previewAudioBlobUrl; |
| uploadArea.classList.add('file-selected'); |
| uploadArea.classList.remove('is-playing', 'active'); |
| iconBox.onclick = togglePreviewPlayback; |
| } |
| } |
| |
| function togglePreviewPlayback(event) { event.stopPropagation(); if (!previewAudio.src) return; if (previewAudio.paused) previewAudio.play(); else previewAudio.pause(); } |
| function resetUploadArea() { document.getElementById('uploadForm').reset(); document.getElementById('fileNameText').innerText = 'کلیک کنید (MP3, WAV)'; uploadArea.classList.remove('file-selected', 'is-playing', 'active'); iconBox.onclick = null; previewAudio.src = ''; if (previewAudioBlobUrl) { URL.revokeObjectURL(previewAudioBlobUrl); previewAudioBlobUrl = null; } } |
| previewAudio.onplay = () => uploadArea.classList.add('is-playing'); |
| previewAudio.onpause = () => uploadArea.classList.remove('is-playing'); |
| previewAudio.onended = () => uploadArea.classList.remove('is-playing'); |
| |
| |
| async function uploadAudioFile(file) { |
| const formData = new FormData(); |
| formData.append('files', file); |
| |
| const response = await fetch('https://opera8-resemble-enhance.hf.space/gradio_api/upload', { |
| method: 'POST', |
| body: formData |
| }); |
| |
| if (!response.ok) { |
| const errText = await response.text(); |
| throw new Error(`خطا در آپلود فایل صوتی: ${response.status} - ${errText}`); |
| } |
| |
| const result = await response.json(); |
| const uploadInfo = result[0]; |
| const filePath = typeof uploadInfo === 'string' ? uploadInfo : uploadInfo.path; |
| return { |
| path: filePath, |
| meta: { _type: "gradio.FileData" }, |
| orig_name: file.name |
| }; |
| } |
| |
| async function startProcessing(payload) { |
| const response = await fetch(`https://opera8-resemble-enhance.hf.space/gradio_api/call/v2/_fn`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify(payload) |
| }); |
| |
| if (!response.ok) { |
| const errText = await response.text(); |
| throw new Error(`پاسخ خطا از سرور مدل: ${response.status} - ${errText}`); |
| } |
| |
| const result = await response.json(); |
| return result.event_id; |
| } |
| |
| |
| document.getElementById('uploadForm').addEventListener('submit', async (e) => { |
| e.preventDefault(); |
| const srcFile = document.getElementById('srcFile').files[0]; |
| if (!srcFile) { alert("لطفاً فایل را انتخاب کنید"); return; } |
| |
| const btn = document.getElementById('submitBtn'); |
| btn.disabled = true; |
| btn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i> در حال آپلود و ارسال فایل...'; |
| |
| try { |
| |
| const fileData = await uploadAudioFile(srcFile); |
| |
| |
| const payload = { |
| path: fileData, |
| solver: document.querySelector('input[name="solver"]:checked').value, |
| nfe: parseInt(document.getElementById('nfe').value), |
| tau: parseFloat(document.getElementById('tau').value), |
| denoising: document.getElementById('denoising').checked |
| }; |
| |
| |
| const eventId = await startProcessing(payload); |
| |
| const jobData = { |
| job_id: eventId, |
| timestamp: new Date().toLocaleTimeString('fa-IR', { hour: '2-digit', minute: '2-digit' }), |
| fileName: srcFile.name, |
| status: 'processing', |
| progress: 0, |
| denoised_url: '', |
| enhanced_url: '' |
| }; |
| |
| |
| saveFileToDB(eventId, srcFile); |
| saveJob(jobData); |
| renderHistory(); |
| |
| btn.disabled = false; |
| btn.innerHTML = 'شروع فرآیند بهبود صدا'; |
| resetUploadArea(); |
| |
| |
| pollJobStatus(eventId); |
| |
| } catch (err) { |
| alert("خطایی رخ داد: " + err.message); |
| btn.disabled = false; |
| btn.innerHTML = 'شروع فرآیند بهبود صدا'; |
| } |
| }); |
| |
| |
| function pollJobStatus(jobId) { |
| fetch(`https://opera8-resemble-enhance.hf.space/gradio_api/call/_fn/${jobId}`) |
| .then(async response => { |
| if (!response.ok) { |
| throw new Error(`خطا در ارتباط با سرور: ${response.status}`); |
| } |
| |
| const reader = response.body.getReader(); |
| const decoder = new TextDecoder("utf-8"); |
| let buffer = ""; |
| let currentEvent = ""; |
| |
| while (true) { |
| const { value, done } = await reader.read(); |
| if (done) break; |
| |
| buffer += decoder.decode(value, { stream: true }); |
| const lines = buffer.split("\n"); |
| buffer = lines.pop(); |
| |
| for (const line of lines) { |
| const trimmed = line.trim(); |
| if (trimmed.startsWith("event:")) { |
| currentEvent = trimmed.substring(6).trim(); |
| } else if (trimmed.startsWith("data:")) { |
| const dataStr = trimmed.substring(5).trim(); |
| let data; |
| try { data = JSON.parse(dataStr); } catch (e) { data = dataStr; } |
| |
| const jobs = getJobs(); |
| if (!jobs[jobId]) return; |
| |
| if (currentEvent === "complete") { |
| jobs[jobId].status = 'completed'; |
| jobs[jobId].progress = 100; |
| |
| const resolveUrl = (audioObj) => { |
| if (!audioObj) return ''; |
| if (audioObj.url) return audioObj.url; |
| return `https://opera8-resemble-enhance.hf.space/file=${audioObj.path}`; |
| }; |
| |
| jobs[jobId].denoised_url = resolveUrl(data[0]); |
| jobs[jobId].enhanced_url = resolveUrl(data[1]); |
| |
| localStorage.setItem(STORAGE_KEY, JSON.stringify(jobs)); |
| renderHistory(); |
| return; |
| |
| } else if (currentEvent === "error") { |
| jobs[jobId].status = 'failed'; |
| localStorage.setItem(STORAGE_KEY, JSON.stringify(jobs)); |
| renderHistory(); |
| return; |
| |
| } else { |
| |
| if (typeof data === 'object' && data.stage) { |
| let progress = jobs[jobId].progress || 10; |
| if (data.stage === 'process') { |
| progress = Math.min(progress + 20, 90); |
| } else if (data.stage === 'pending') { |
| progress = Math.min(progress + 5, 40); |
| } |
| jobs[jobId].progress = progress; |
| |
| const queueMsgEl = document.getElementById(`queue-message-${jobId}`); |
| if (queueMsgEl) { |
| if (data.queue_size && data.queue_size > 0) { |
| queueMsgEl.innerHTML = `<i class="fas fa-hourglass-half fa-spin-pulse"></i> تعداد ${data.queue_size} درخواست قبل از شما در صف است...`; |
| queueMsgEl.classList.add('visible'); |
| } else { |
| queueMsgEl.classList.remove('visible'); |
| } |
| } |
| } else { |
| let progress = jobs[jobId].progress || 10; |
| progress = Math.min(progress + 3, 95); |
| jobs[jobId].progress = progress; |
| } |
| localStorage.setItem(STORAGE_KEY, JSON.stringify(jobs)); |
| updateProgressBarDOM(jobId, jobs[jobId].progress); |
| } |
| } |
| } |
| } |
| }) |
| .catch(err => { |
| console.error("پیگیری با خطا مواجه شد:", err); |
| const jobs = getJobs(); |
| if (jobs[jobId]) { |
| jobs[jobId].status = 'failed'; |
| localStorage.setItem(STORAGE_KEY, JSON.stringify(jobs)); |
| renderHistory(); |
| } |
| }); |
| } |
| |
| function resumeActivePolls() { |
| const jobs = getJobs(); |
| for (const id in jobs) { |
| if (jobs[id].status === 'processing') { |
| pollJobStatus(id); |
| } |
| } |
| } |
| |
| function updateProgressBarDOM(id, percent) { |
| const bar = document.getElementById(`progress-fill-${id}`); |
| const text = document.getElementById(`progress-text-${id}`); |
| if (bar) bar.style.width = `${percent}%`; |
| if (text && text.innerHTML.includes('%')) text.innerHTML = `${percent}%`; |
| } |
| |
| |
| function renderHistory() { |
| const list = document.getElementById('historyList'); |
| const jobs = Object.values(getJobs()).reverse(); |
| list.innerHTML = ''; |
| if (jobs.length === 0) { |
| list.innerHTML = '<div class="text-center text-muted py-5 small"><i class="fas fa-box-open fa-2x mb-2 opacity-50"></i><br>هنوز فایلی پردازش نشده</div>'; |
| return; |
| } |
| const totalJobs = jobs.length; |
| jobs.forEach((job, index) => { |
| const requestNumber = totalJobs - index; |
| const cardWrapper = document.createElement('div'); |
| let title, description, percentageText, buttonText, buttonAction, buttonIcon; |
| |
| if (job.status === 'processing') { |
| title = 'در حال بهبود کیفیت صدا'; |
| description = 'سرور صوتی در حال اعمال پردازشها است. میتوانید از صفحه خارج شوید.'; |
| percentageText = `${job.progress || 0}%`; |
| buttonText = 'در حال پیگیری...'; |
| buttonIcon = '<i class="fas fa-clock me-2"></i>'; |
| buttonAction = ''; |
| } else if (job.status === 'completed') { |
| title = 'بهبود صدا با موفقیت تکمیل شد'; |
| description = `نام فایل اصلی: <strong>${job.fileName}</strong>`; |
| percentageText = 'تکمیل شد'; |
| buttonText = 'نمایش فایلهای نهایی'; |
| buttonIcon = '<i class="fas fa-play-circle me-2"></i>'; |
| buttonAction = `onclick="toggleHistoryItem('${job.job_id}')"`; |
| } else { |
| title = 'خطا در حین فرآیند بهبود صدا'; |
| description = 'پردازش فایل صوتی شما روی سرور به دلیل خطا ناموفق بود.'; |
| percentageText = 'خطا'; |
| buttonText = 'خطا در پردازش'; |
| buttonIcon = '<i class="fas fa-exclamation-triangle me-2"></i>'; |
| buttonAction = ''; |
| } |
| |
| cardWrapper.innerHTML = ` |
| <div class="history-card ${job.status}" id="history-${job.job_id}"> |
| <div class="history-card-header"> |
| <div class="history-card-title"> |
| <h6>${title}</h6> |
| <span class="request-badge">درخواست شماره ${requestNumber}</span> |
| </div> |
| <div class="status-description">${description}</div> |
| <div class="progress-display"> |
| <div class="progress-percentage" id="progress-text-${job.job_id}">${percentageText}</div> |
| <div class="queue-status-message" id="queue-message-${job.job_id}"> |
| <i class="fas fa-hourglass-half fa-spin-pulse" style="--fa-animation-duration: 2s;"></i> درخواست در صف انجام است، لطفاً صبور باشید... |
| </div> |
| <div class="progress-bar-custom"> |
| <div class="progress-bar-custom-fill" id="progress-fill-${job.job_id}" style="width: ${job.progress || 0}%"></div> |
| </div> |
| </div> |
| <div class="action-buttons"> |
| <button class="btn-delete-icon" onclick="openDeleteModal('${job.job_id}')"><i class="fas fa-trash-alt"></i></button> |
| <button class="btn-status" ${buttonAction}>${buttonIcon} ${buttonText}</button> |
| </div> |
| </div> |
| ${job.status === 'completed' ? `<div class="history-card-body">${getExpandedContent(job)}</div>` : ''} |
| </div> |
| ${index < jobs.length - 1 ? '<hr class="history-separator">' : ''} |
| `; |
| list.appendChild(cardWrapper); |
| }); |
| } |
| |
| function getExpandedContent(job) { |
| getFileFromDB(job.job_id, (blob) => { |
| if (blob) { |
| const audioPlayer = document.getElementById(`original-audio-${job.job_id}`); |
| if (audioPlayer) audioPlayer.src = URL.createObjectURL(blob); |
| } |
| }); |
| return ` |
| <div class="audio-player-wrapper"> |
| <small class="d-block fw-bold text-muted mb-1" style="font-size:0.8rem">فایل صوتی اصلی شما:</small> |
| <audio controls id="original-audio-${job.job_id}" class="mini-player"></audio> |
| </div> |
| <div class="audio-player-wrapper mt-3"> |
| <small class="d-block fw-bold text-info mb-1" style="font-size:0.8rem">فایل خروجی کاهش نویز (Denoised Audio):</small> |
| <audio controls src="${job.denoised_url}" class="mini-player"></audio> |
| </div> |
| <div class="audio-player-wrapper mt-3"> |
| <small class="d-block fw-bold text-success mb-1" style="font-size:0.8rem">فایل خروجی بهبودیافته نهایی (Enhanced Audio):</small> |
| <audio controls src="${job.enhanced_url}" class="mini-player"></audio> |
| </div> |
| |
| <div class="row g-2 mt-3"> |
| <div class="col-6"> |
| <button type="button" class="btn btn-sm btn-outline-info w-100 rounded-pill fw-bold btn-download-via-postmessage" data-url="${job.denoised_url}"> |
| <i class="fas fa-download me-1"></i> دانلود نسخه بدون نویز |
| </button> |
| </div> |
| <div class="col-6"> |
| <button type="button" class="btn btn-sm btn-success w-100 rounded-pill fw-bold btn-download-via-postmessage" data-url="${job.enhanced_url}"> |
| <i class="fas fa-download me-1"></i> دانلود نسخه استودیویی |
| </button> |
| </div> |
| </div> |
| `; |
| } |
| |
| function toggleHistoryItem(id) { document.getElementById(`history-${id}`).classList.toggle('expanded'); } |
| |
| |
| let deleteId = null; |
| function openDeleteModal(id) { deleteId = id; document.getElementById('deleteModal').style.display = 'flex'; } |
| function closeModal() { document.getElementById('deleteModal').style.display = 'none'; deleteId = null; } |
| function closeTimeLimitModal() { document.getElementById('timeLimitModal').style.display = 'none'; } |
| |
| document.getElementById('confirmDeleteBtn').onclick = () => { |
| if(deleteId === 'ALL') { |
| localStorage.removeItem(STORAGE_KEY); |
| clearDB(); |
| } else { |
| const jobs = getJobs(); |
| delete jobs[deleteId]; |
| localStorage.setItem(STORAGE_KEY, JSON.stringify(jobs)); |
| deleteFileFromDB(deleteId); |
| } |
| renderHistory(); |
| closeModal(); |
| }; |
| |
| document.getElementById('upgradeFromModalBtn').onclick = () => { parent.postMessage({ type: 'NAVIGATE_TO_PREMIUM', payload: { url: PREMIUM_URL } }, '*'); }; |
| |
| |
| function isUserPaid(userObject) { return userObject && userObject.isLogin && userObject.accessible_pages && (userObject.accessible_pages.includes(PREMIUM_PAGE_ID) || userObject.accessible_pages.includes(parseInt(PREMIUM_PAGE_ID))); } |
| |
| function updateSubscriptionBadge(status) { |
| const badge = document.getElementById('subscription-status-badge'); |
| if (!badge) return; |
| if (status === 'paid') { |
| badge.textContent = 'نسخه نامحدود'; |
| badge.className = 'paid-badge'; |
| } else { |
| badge.textContent = 'نسخه رایگان'; |
| badge.className = 'free-badge'; |
| } |
| badge.style.display = 'inline-block'; |
| } |
| |
| window.addEventListener('message', (event) => { |
| if (event.data && event.data.type === 'USER_STATUS_RESPONSE') { |
| try { |
| const userObject = JSON.parse(event.data.payload); |
| userSubscriptionStatus = isUserPaid(userObject) ? 'paid' : 'free'; |
| } catch (e) { |
| userSubscriptionStatus = 'free'; |
| } |
| updateSubscriptionBadge(userSubscriptionStatus); |
| } |
| }); |
| |
| document.getElementById('historyList').addEventListener('click', (e) => { |
| const downloadButton = e.target.closest('.btn-download-via-postmessage'); |
| if(downloadButton) { |
| e.preventDefault(); |
| const audioUrl = downloadButton.dataset.url; |
| if(audioUrl) { |
| parent.postMessage({ type: 'INITIATE_DOWNLOAD_FROM_URL', payload: { audioUrl: audioUrl } }, '*'); |
| const originalText = downloadButton.innerHTML; |
| downloadButton.innerHTML = '<i class="fas fa-check me-1"></i> ارسال شد'; |
| setTimeout(() => { downloadButton.innerHTML = originalText; }, 2000); |
| } |
| } |
| }); |
| |
| |
| initDB(); |
| parent.postMessage({ type: 'REQUEST_USER_STATUS' }, '*'); |
| </script> |
| </body> |
| </html> |