tagviat / index.html
Opera10's picture
Update index.html
c5fc8db verified
Raw
History Blame Contribute Delete
48.5 kB
<!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;
// --- مدیریت بانک اطلاعاتی محلی مرورگر (IndexedDB) برای حفظ موقت فایل صوتی اصلی ---
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');
// --- توابع ارتباط مستقیم با API مدل صوتی (Gradio-HuggingFace Space) ---
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 = 'شروع فرآیند بهبود صدا';
}
});
// --- مدیریت و پیگیری وضعیت کارهای صوتی از طریق کانال ارتباطی SSE ---
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>