mp3forvideo / index.html
Elias207's picture
Update index.html
0b42529 verified
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>مولد صدای هوشمند MMAudio</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/rastikerdar/vazirmatn@v33.003/Vazirmatn-font-face.css">
<style>
/* ===== THEME & STYLES FROM QR CODE APP ===== */
: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-smooth: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
/* --- NEW Variables for Error Guide --- */
--primary-gradient-guide: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--success-gradient-guide: linear-gradient(135deg, #56ab2f 0%, #a8e063 100%);
--guide-bg: rgba(255, 255, 255, 0.98);
--guide-border: rgba(102, 126, 234, 0.2);
--guide-text-title: #2d3748;
--guide-text-body: #4a5568;
--guide-accent: #667eea;
--radius-md-guide: 12px;
--radius-lg-guide: 20px;
--shadow-md-guide: 0 4px 6px -1px rgba(26, 32, 44, 0.05), 0 2px 4px -2px rgba(26, 32, 44, 0.04);
--shadow-lg-guide: 0 10px 15px -3px rgba(26, 32, 44, 0.06), 0 4px 6px -4px rgba(26, 32, 44, 0.05);
--shadow-xl-guide: 0 20px 25px -5px rgba(26, 32, 44, 0.07), 0 8px 10px -6px rgba(26, 32, 44, 0.05);
--transition-smooth-guide: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}
/* ===== BASE STYLES ===== */
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: var(--app-font);
background-color: var(--app-bg);
color: var(--text-primary);
min-height: 100vh;
overflow-x: hidden;
display: flex;
justify-content: center;
align-items: flex-start;
padding: 2.5rem 1rem;
background-image: radial-gradient(var(--text-tertiary) 0.5px, transparent 0.5px);
background-size: 20px 20px;
background-position: -10px -10px;
}
.container { max-width: 600px; width: 100%; margin: 0 auto; display: flex; flex-direction: column; }
/* ===== HEADER ===== */
.header { text-align: center; padding: 1rem 0 2rem; }
.logo { font-size: 3rem; margin-bottom: 10px; filter: drop-shadow(0 4px 8px rgba(0,0,0,0.1)); }
.title { font-size: 2.2rem; font-weight: 900; margin-bottom: 0.8rem; background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; letter-spacing: -1px; }
.subtitle { font-size: 1rem; color: var(--text-secondary); opacity: 0.9; }
/* ===== TABS ===== */
.tabs { display: flex; background: var(--input-bg); border-radius: var(--radius-btn); padding: 6px; margin-bottom: 20px; border: 1px solid var(--panel-border); }
.tab { flex: 1; padding: 12px; text-align: center; border-radius: 10px; cursor: pointer; transition: var(--transition-smooth); font-weight: 600; font-size: 0.9rem; color: var(--text-secondary); }
.tab.active { background: var(--panel-bg); color: var(--text-primary); transform: translateY(-2px); box-shadow: var(--shadow-lg); }
/* ===== CARD ===== */
.card { background: var(--panel-bg); border-radius: var(--radius-card); padding: 30px; border: 1px solid var(--panel-border); box-shadow: var(--shadow-xl); display: none; flex-direction: column; }
.tab-content.active { display: flex; }
.form-group { margin-bottom: 20px; }
.label { display: block; margin-bottom: 10px; font-size: 1rem; font-weight: 700; color: var(--text-primary); }
/* ===== INPUTS & UPLOAD AREA ===== */
.input { width: 100%; padding: 15px; border: 1px solid var(--input-border); border-radius: var(--radius-input); background: var(--input-bg); color: var(--text-primary); font-size: 1rem; font-family: var(--app-font); outline: none; transition: var(--transition-smooth); box-shadow: var(--shadow-sm) inset; }
.input:focus { background: var(--panel-bg); border-color: var(--accent-primary); box-shadow: 0 0 0 3px var(--accent-primary-glow), var(--shadow-sm) inset; }
.input-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; }
.file-upload { position: relative; background: var(--input-bg); border: 2px dashed var(--input-border); border-radius: var(--radius-input); padding: 20px; text-align: center; cursor: pointer; transition: var(--transition-smooth); min-height: 160px; display: flex; flex-direction: column; align-items: center; justify-content: center; overflow: hidden; }
.file-upload:hover, .file-upload.drag-over { background: white; border-color: var(--accent-primary); box-shadow: 0 0 15px var(--accent-primary-glow); transform: translateY(-2px); }
.file-upload input { position: absolute; left: -9999px; }
.upload-icon { font-size: 2.5rem; margin-bottom: 10px; color: var(--accent-primary); }
.upload-text { color: var(--text-secondary); font-size: 0.9rem; font-weight: 500; }
.file-upload.has-preview { border-style: solid; border-color: var(--accent-primary); padding: 10px; cursor: default; }
.preview-container { position: relative; width: 100%; height: 100%; }
.preview-container video { width: 100%; max-height: 250px; border-radius: var(--radius-input); display: block; }
.remove-btn { position: absolute; top: 10px; left: 10px; width: 32px; height: 32px; background-color: rgba(0, 0, 0, 0.6); color: white; border: none; border-radius: 50%; font-size: 20px; font-weight: bold; cursor: pointer; display: flex; align-items: center; justify-content: center; line-height: 1; transition: var(--transition-smooth); z-index: 10; }
.remove-btn:hover { background-color: #e53e3e; transform: scale(1.1); }
/* ===== BUTTONS ===== */
.btn { width: 100%; padding: 16px; border: none; border-radius: var(--radius-btn); font-size: 1.1rem; font-weight: 700; cursor: pointer; margin-top: 10px; transition: var(--transition-smooth); background: linear-gradient(95deg, var(--accent-secondary) 0%, var(--accent-primary) 100%); color: white; box-shadow: 0 6px 12px -3px var(--accent-primary-glow), 0 6px 12px -3px var(--accent-secondary-glow); }
.btn:hover:not(:disabled) { transform: translateY(-5px) scale(1.02); box-shadow: 0 8px 20px -4px var(--accent-primary-glow), 0 8px 20px -4px var(--accent-secondary-glow); }
.btn:disabled { background: var(--text-tertiary); color: var(--text-secondary); cursor: not-allowed; transform: none; box-shadow: none; opacity: 0.7; }
.btn:active:not(:disabled) { transform: translateY(0); }
/* ===== NEW: DOWNLOAD BUTTON STYLE ===== */
.download-btn {
display: inline-flex; /* Changed to inline-flex for better alignment */
align-items: center;
justify-content: center;
gap: 10px; /* Space between icon and text */
padding: 12px 25px;
margin-top: 20px;
width: auto; /* Not full width */
border: none;
border-radius: var(--radius-btn);
font-size: 1rem;
font-weight: 700;
cursor: pointer;
transition: var(--transition-smooth);
background: var(--accent-primary);
color: white;
box-shadow: 0 6px 12px -3px var(--accent-primary-glow);
}
.download-btn:hover {
background: var(--accent-primary-hover);
transform: translateY(-3px);
box-shadow: 0 8px 15px -4px var(--accent-primary-glow);
}
.download-btn .icon {
font-size: 1.2rem;
margin: 0; /* Remove default margin */
}
/* ===== RESULT AREA ===== */
.result { margin-top: 20px; text-align: center; }
.status-text { color: var(--text-secondary); font-weight: 500; margin: 20px 0; line-height: 1.6; }
.status-text.error { color: #e53e3e; font-weight: 600; }
.status-text.success { color: #38a169; font-weight: 600; margin-bottom: 15px; } /* Adjusted margin */
audio, video { width: 100%; margin-top: 10px; border-radius: var(--radius-input); box-shadow: var(--shadow-md); outline: none; }
.loader { width: 40px; height: 40px; border: 4px solid var(--input-border); border-top: 4px solid var(--accent-primary); border-radius: 50%; animation: spin 1s linear infinite; margin: 20px auto; }
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
.icon { display: inline-block; margin-left: 8px; vertical-align: middle; }
/* ===== ERROR INSTRUCTIONS BOX (Kept for non-GPU errors if needed, but GPU error is replaced) ===== */
.error-instructions-box { display: none; margin-top: 25px; /* Base styles */ }
/* --- NEW Styles for GPU Error Guide --- */
#vta-gpu-error-box, #tta-gpu-error-box { padding: 0 !important; border: none !important; background: transparent !important; }
.ip-reset-guide-container { text-align: right; background: var(--guide-bg); backdrop-filter: blur(10px); padding: 25px; border-radius: var(--radius-lg-guide); box-shadow: var(--shadow-xl-guide); border: 1px solid var(--guide-border); animation: slideInUp 0.6s cubic-bezier(0.4, 0, 0.2, 1) both; max-width: 650px; width: 100%; position: relative; overflow: hidden; margin: 0 auto; }
.ip-reset-guide-container::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 5px; background: var(--primary-gradient-guide); }
.guide-header { display: flex; align-items: center; margin-bottom: 20px; }
.guide-header-icon { width: 50px; height: 50px; margin-left: 15px; animation: float 3s ease-in-out infinite; }
.guide-header h2 { font-size: 1.3rem; color: var(--guide-text-title); font-weight: 700; margin: 0; }
.guide-header p { color: var(--guide-text-body); font-size: 0.8rem; margin-top: 5px; margin-bottom: 0; }
.guide-content { font-size: 0.9rem; color: var(--guide-text-body); line-height: 1.8; }
.info-card { background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%); border: 1px solid rgba(102, 126, 234, 0.2); border-radius: var(--radius-md-guide); padding: 15px; margin: 15px 0; position: relative; overflow: hidden; }
.info-card p { font-size: 0.85rem; line-height: 1.7; margin: 0; }
.info-card::before { content: ''; position: absolute; top: 0; right: 0; width: 4px; height: 100%; background: var(--primary-gradient-guide); }
.info-card-header { display: flex; align-items: center; margin-bottom: 12px; }
.info-card-icon { width: 22px; height: 22px; margin-left: 10px; }
.info-card-title { font-weight: 600; color: var(--guide-text-title); font-size: 0.95rem; }
.summary-section { margin-top: 20px; padding: 15px; border-radius: var(--radius-md-guide); background: linear-gradient(135deg, #56ab2f15 0%, #a8e06315 100%); border: 1px solid rgba(86, 171, 47, 0.2); position: relative; overflow: hidden; }
.summary-section::before { content: ''; position: absolute; top: 0; right: 0; width: 4px; height: 100%; background: var(--success-gradient-guide); }
.summary-header { display: flex; align-items: center; margin-bottom: 10px; }
.summary-icon { width: 22px; height: 22px; margin-left: 10px; }
.summary-title { font-weight: 600; color: #2f5a33; font-size: 0.95rem; }
.summary-text { color: #2f5a33; font-size: 0.85rem; line-height: 1.7; }
.video-button-container { text-align: center; margin: 20px 0 10px 0; }
.elegant-video-button { display: inline-flex; align-items: center; justify-content: center; padding: 7px 18px; background-color: #f0f2f5; color: var(--guide-accent); border: 1px solid #e2e8f0; text-decoration: none; border-radius: var(--radius-md-guide); font-weight: 600; font-size: 0.8rem; cursor: pointer; transition: all 0.3s ease; box-shadow: var(--shadow-sm); }
.elegant-video-button:hover { background: var(--primary-gradient-guide); color: white; border-color: transparent; transform: translateY(-2px); box-shadow: 0 6px 16px rgba(102, 126, 234, 0.3); }
.elegant-video-button-icon { width: 16px; height: 16px; margin-left: 8px; fill: currentColor; }
.guide-actions { display: flex; gap: 15px; margin-top: 25px; padding-top: 20px; border-top: 1px solid #e2e8f0; }
.guide-actions .action-button { padding: 12px 20px; border: none; border-radius: var(--radius-md-guide); font-size: 0.9rem; font-weight: 600; cursor: pointer; flex: 1; transition: var(--transition-smooth-guide); position: relative; overflow: hidden; display: flex; align-items: center; justify-content: center; }
.action-button-icon { width: 18px; height: 18px; margin-right: 0; margin-left: 8px; }
.back-button { background: white; color: var(--guide-text-body); border: 2px solid #e2e8f0; flex: 0.4; }
.back-button:hover { background: #f7fafc; border-color: var(--guide-accent); transform: translateY(-2px); box-shadow: var(--shadow-md-guide); }
.retry-button { background: var(--primary-gradient-guide); color: white; flex: 0.6; box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3); }
.retry-button:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4); }
@keyframes slideInUp { from { opacity: 0; transform: translateY(30px); } to { opacity: 1; transform: translateY(0); } }
@keyframes float { 0%, 100% { transform: translateY(0px); } 50% { transform: translateY(-10px); } }
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="logo">🎧</div>
<h1 class="title">مولد صدای هوشمند</h1>
<p class="subtitle">با کمک هوش مصنوعی از متن یا ویدیو، صدا بسازید</p>
</div>
<div class="tabs">
<div class="tab active" data-tab="video-to-audio"><span class="icon">🎬</span>صدا برای ویدیو</div>
<div class="tab" data-tab="text-to-audio"><span class="icon">✏️</span>متن به صدا</div>
</div>
<!-- بخش ساخت صدا برای ویدیو -->
<div class="card tab-content active" id="video-to-audio-content">
<div class="form-group">
<label class="label">فایل ویدیو را انتخاب کنید:</label>
<div class="file-upload" id="vta-file-upload-area">
<input type="file" id="vta-video" accept="video/*">
<div class="upload-content">
<div class="upload-icon">📹</div>
<div class="upload-text">برای انتخاب کلیک کنید یا فایل را اینجا بکشید</div>
</div>
</div>
</div>
<div class="form-group">
<label class="label" for="vta-prompt">متن اصلی (اختیاری):</label>
<input type="text" id="vta-prompt" class="input" placeholder="می‌توانید برای هدایت بهتر، صدا را توصیف کنید">
</div>
<div class="form-group">
<label class="label" for="vta-negative-prompt">متن منفی (مواردی که نمی‌خواهید) به فارسی:</label>
<input type="text" id="vta-negative-prompt" class="input" placeholder="مثلا: موسیقی، صدای انسان">
</div>
<div class="input-grid">
<div class="form-group">
<label class="label" for="vta-seed">Seed:</label>
<input type="number" id="vta-seed" class="input" value="-1">
</div>
<div class="form-group">
<label class="label" for="vta-duration">مدت (ثانیه):</label>
<input type="number" id="vta-duration" class="input" value="8">
</div>
</div>
<div id="vta-result" class="result"></div>
<button id="generate-video-audio" class="btn"><span class="icon">🪄</span>ایجاد صدا برای ویدیو</button>
<div id="vta-gpu-error-box" class="error-instructions-box"></div>
</div>
<!-- بخش تبدیل متن به صدا -->
<div class="card tab-content" id="text-to-audio-content">
<div class="form-group">
<label class="label" for="tta-prompt">متن اصلی (توضیح صدا) به فارسی:</label>
<input type="text" id="tta-prompt" class="input" placeholder="مثلا: صدای امواج دریا و مرغان دریایی">
</div>
<div class="form-group">
<label class="label" for="tta-negative-prompt">متن منفی (مواردی که نمی‌خواهید) به فارسی:</label>
<input type="text" id="tta-negative-prompt" class="input" placeholder="مثلا: موسیقی، نویز زیاد">
</div>
<div class="input-grid">
<div class="form-group">
<label class="label" for="tta-seed">Seed:</label>
<input type="number" id="tta-seed" class="input" value="-1">
</div>
<div class="form-group">
<label class="label" for="tta-duration">مدت (ثانیه):</label>
<input type="number" id="tta-duration" class="input" value="8">
</div>
</div>
<div id="tta-result" class="result"></div>
<button id="generate-text-audio" class="btn"><span class="icon"></span>ایجاد صدا</button>
<div id="tta-gpu-error-box" class="error-instructions-box"></div>
</div>
</div>
<script type="module">
import { client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
const tabs = document.querySelectorAll('.tab');
const tabContents = document.querySelectorAll('.tab-content');
// --- NEW: GPU Quota Instructions HTML Template ---
const GPU_QUOTA_INSTRUCTIONS_HTML = `
<div class="ip-reset-guide-container">
<div class="guide-header"><svg class="guide-header-icon" viewbox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"> <defs><lineargradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color: #667eea; stop-opacity: 1;"></stop><stop offset="100%" style="stop-color: #764ba2; stop-opacity: 1;"></stop></lineargradient></defs> <circle cx="50" cy="50" r="45" fill="url(#grad1)" opacity="0.1"></circle><circle cx="50" cy="50" r="35" fill="none" stroke="url(#grad1)" stroke-width="2" opacity="0.3"></circle><path d="M35 50 L45 60 L65 40" stroke="url(#grad1)" stroke-width="4" fill="none" stroke-linecap="round" stroke-linejoin="round"></path><circle cx="65" cy="35" r="8" fill="#fee140"></circle><path d="M62 35 L68 35 M65 32 L65 38" stroke="white" stroke-width="2" stroke-linecap="round"></path> </svg>
<div>
<h2>یک قدم تا ساخت صدای جدید</h2>
<p>نیازمند تغییر نقطه دستیابی</p>
</div>
</div>
<div class="guide-content">
<div class="info-card">
<div class="info-card-header"><svg class="info-card-icon" viewbox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" fill="#667eea" opacity="0.2"></path><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" stroke="#667eea" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg> <span class="info-card-title">راه حل سریع</span></div>
<p>طبق ویدیو آموزشی پایین بین نقطه دستیابی جابجا شوید تلاش مجدد بزنید تا صدا مجدداً تولید بشه.</p>
</div>
<div class="summary-section">
<div class="summary-header"><svg class="summary-icon" viewbox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="10" fill="#56ab2f" opacity="0.2"></circle><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z" stroke="#56ab2f" stroke-width="2"></path><path d="M9 12l2 2 4-4" stroke="#56ab2f" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg> <span class="summary-title">خلاصه راهنما</span></div>
<div class="summary-text">هربار که این صفحه را مشاهده کردید: از اینترنت سیم‌کارت استفاده کنید، VPN را خاموش کرده و طبق ویدیو آموزشی پایین نقطه دستیابی رو تغییر دهید. &laquo;تلاش مجدد&raquo; کلیک کنید. با این روش ساده می‌توانید به صورت نامحدود صدا بسازید! ☘️</div>
</div>
<div class="video-button-container"><button id="tutorialLinkBtn" class="elegant-video-button"> <svg class="elegant-video-button-icon" xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M8 5v14l11-7z"></path></svg> <span>دیدن ویدیو آموزشی استفاده نامحدود</span> </button></div>
</div>
<div class="guide-actions">
<button class="action-button back-button"> <svg class="action-button-icon" viewbox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M19 12H5M12 19l-7-7 7-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg> <span>بازگشت</span> </button>
<button class="action-button retry-button"> <svg class="action-button-icon" viewbox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M23 4v6h-6M1 20v-6h6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path><path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg> <span>تلاش مجدد</span> </button>
</div>
</div>`;
// --- NEW: GPU Timeout Error HTML Template ---
const GPU_TIMEOUT_ERROR_HTML = `
<div class="ip-reset-guide-container">
<div class="guide-header">
<div style="font-size: 2.5rem; margin-left: 15px;">⚠️</div>
<div>
<h2>زمان زیاد</h2>
<p>زمان انتظار بیش از حد مجاز</p>
</div>
</div>
<div class="guide-content">
<div class="info-card" style="border-left: 4px solid #f6ad55; background: linear-gradient(135deg, #f6ad5515 0%, #ed893615 100%);">
<div class="info-card-header">
<span class="info-card-title" style="color: #7b341e;">توجه</span>
</div>
<p style="font-size: 1rem; font-weight: 500; color: #2d3748; line-height: 2;">
لطفاً نقطه دستیابی رو عوض کنید و یک ویدیو با زمان کمتر از بیست ثانیه بررسی کنید
</p>
</div>
<div class="guide-actions">
<button class="action-button back-button"> <span>بازگشت</span> </button>
<button class="action-button retry-button"> <span>تلاش مجدد</span> </button>
</div>
</div>
</div>`;
// --- TAB SWITCHING LOGIC ---
tabs.forEach(tab => {
tab.addEventListener('click', () => {
tabs.forEach(t => t.classList.remove('active'));
tabContents.forEach(tc => tc.classList.remove('active'));
tab.classList.add('active');
document.getElementById(tab.getAttribute('data-tab') + '-content').classList.add('active');
});
});
// --- CUSTOM FILE UPLOAD LOGIC ---
const vtaFileInput = document.getElementById('vta-video');
const vtaFileUploadArea = document.getElementById('vta-file-upload-area');
const vtaUploadContent = vtaFileUploadArea.querySelector('.upload-content');
vtaFileUploadArea.addEventListener('click', (e) => {
if (!vtaFileUploadArea.classList.contains('has-preview') || e.target === vtaFileUploadArea) {
vtaFileInput.click();
}
});
const handleFileSelect = () => {
const file = vtaFileInput.files[0];
const existingPreview = vtaFileUploadArea.querySelector('.preview-container');
if (existingPreview) existingPreview.remove();
if (file) {
const fileURL = URL.createObjectURL(file);
const previewContainer = document.createElement('div');
previewContainer.className = 'preview-container';
const videoPreview = document.createElement('video');
videoPreview.src = fileURL;
videoPreview.controls = true;
videoPreview.muted = true;
const removeBtn = document.createElement('button');
removeBtn.className = 'remove-btn';
removeBtn.innerHTML = '&times;';
removeBtn.onclick = (e) => {
e.stopPropagation();
vtaFileInput.value = '';
previewContainer.remove();
vtaUploadContent.style.display = 'flex';
vtaFileUploadArea.classList.remove('has-preview');
};
previewContainer.appendChild(videoPreview);
previewContainer.appendChild(removeBtn);
vtaUploadContent.style.display = 'none';
vtaFileUploadArea.appendChild(previewContainer);
vtaFileUploadArea.classList.add('has-preview');
} else {
vtaUploadContent.style.display = 'flex';
vtaFileUploadArea.classList.remove('has-preview');
}
};
vtaFileInput.addEventListener('change', handleFileSelect);
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => vtaFileUploadArea.addEventListener(eventName, e => { e.preventDefault(); e.stopPropagation(); }));
['dragenter', 'dragover'].forEach(eventName => vtaFileUploadArea.addEventListener(eventName, () => vtaFileUploadArea.classList.add('drag-over')));
['dragleave', 'drop'].forEach(eventName => vtaFileUploadArea.addEventListener(eventName, () => vtaFileUploadArea.classList.remove('drag-over')));
vtaFileUploadArea.addEventListener('drop', e => {
if (e.dataTransfer.files.length > 0) {
vtaFileInput.files = e.dataTransfer.files;
handleFileSelect();
}
});
// --- TRANSLATION LOGIC ---
const TRANSLATOR_API_BASE_URL = "https://hamed744-translate-tts-aloha.hf.space";
const TRANSLATOR_FN_INDEX = 1;
const TRANSLATOR_OTHER_PARAMS = ["انگلیسی (آمریکا) - جنی (زن)", 0, 0, 0];
function generateSessionHash() { return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); }
async function callGradioApi(baseUrl, fnIndex, dataPayload, sessionHash) {
const response = await fetch(`${baseUrl}/gradio_api/queue/join`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ data: dataPayload, event_data: null, fn_index: fnIndex, session_hash: sessionHash }) });
if (!response.ok) { const errorText = await response.text(); throw new Error(`Error joining queue: ${errorText}`); }
const result = await response.json();
if (!result.event_id) { throw new Error("event_id not received from /queue/join."); }
return result.event_id;
}
function listenToGradioSse(baseUrl, sessionHash, eventId) {
return new Promise((resolve, reject) => {
const eventSource = new EventSource(`${baseUrl}/gradio_api/queue/data?session_hash=${sessionHash}`);
eventSource.onmessage = function(event) {
const eventData = JSON.parse(event.data);
if (eventData.msg === "process_completed") {
eventSource.close();
if (eventData.success && eventData.output && eventData.output.data) { resolve(eventData.output.data); }
else { reject(new Error(eventData.output?.error || "Translation failed.")); }
} else if (eventData.msg === "queue_full") {
eventSource.close(); reject(new Error("Translation service queue is full."));
}
};
eventSource.onerror = function() { eventSource.close(); reject(new Error("Connection error with translation service.")); };
});
}
async function translateText(persianText) {
if (!persianText || !persianText.trim()) return "";
const sessionHash = generateSessionHash();
const payload = [persianText, ...TRANSLATOR_OTHER_PARAMS];
try {
const eventId = await callGradioApi(TRANSLATOR_API_BASE_URL, TRANSLATOR_FN_INDEX, payload, sessionHash);
const translationResult = await listenToGradioSse(TRANSLATOR_API_BASE_URL, sessionHash, eventId);
if (translationResult && translationResult[0] && typeof translationResult[0] === 'string') { return translationResult[0]; }
else { throw new Error("Invalid response from translation service."); }
} catch (error) { console.error("Translation Error:", error); throw new Error(`خطا در ترجمه: ${error.message}`); }
}
// --- NEW: Helper function to create the download button ---
function createDownloadButton(fileUrl) {
const button = document.createElement('button');
button.className = 'download-btn';
button.innerHTML = `<span class="icon">⬇️</span> دانلود`;
button.onclick = () => {
console.log('Sending download request for URL:', fileUrl);
// Send message to parent window (your Android WebView)
parent.postMessage({
type: 'DOWNLOAD_REQUEST',
url: fileUrl
}, '*'); // Use '*' for targetOrigin in this specific WebView context
};
return button;
}
// --- Text-to-Audio Logic (MODIFIED) ---
const ttaButton = document.getElementById('generate-text-audio');
const ttaResult = document.getElementById('tta-result');
const ttaGpuErrorBox = document.getElementById('tta-gpu-error-box');
ttaButton.addEventListener('click', async () => {
const persianPrompt = document.getElementById('tta-prompt').value;
const persianNegativePrompt = document.getElementById('tta-negative-prompt').value;
if (!persianPrompt) { alert('لطفا متن اصلی را وارد کنید.'); return; }
ttaButton.disabled = true;
ttaGpuErrorBox.style.display = 'none';
ttaResult.innerHTML = '<div class="loader"></div><p class="status-text">۱. در حال ارسال دستور...</p>';
try {
const [englishPrompt, englishNegativePrompt] = await Promise.all([ translateText(persianPrompt), translateText(persianNegativePrompt) ]);
ttaResult.innerHTML = `<div class="loader"></div><p class="status-text">۲. دستور ارسال شد.<br>در حال پردازش و ساخت صدا...</p>`;
const app = await client("hkchengrex/MMAudio");
const result = await app.predict("/predict_1", [ englishPrompt, englishNegativePrompt, Number(document.getElementById('tta-seed').value), 25, 4.5, Number(document.getElementById('tta-duration').value) ]);
// --- MODIFIED SUCCESS BLOCK ---
ttaResult.innerHTML = ''; // Clear the loader
const successText = document.createElement('p');
successText.className = 'status-text success';
successText.textContent = 'صدا با موفقیت ایجاد شد!';
const audioPlayer = document.createElement('audio');
audioPlayer.controls = true;
audioPlayer.src = result.data[0].url;
const downloadBtn = createDownloadButton(result.data[0].url);
ttaResult.appendChild(successText);
ttaResult.appendChild(audioPlayer);
ttaResult.appendChild(downloadBtn);
// --- END MODIFIED BLOCK ---
} catch (error) {
console.error("Text-to-Audio Error:", error);
ttaResult.innerHTML = '';
if (error.message && (error.message.includes("exceeded your GPU quota") || error.message.toLowerCase().includes("capacity"))) {
ttaGpuErrorBox.innerHTML = GPU_QUOTA_INSTRUCTIONS_HTML;
ttaGpuErrorBox.style.display = 'block';
// Add event listeners for the new buttons
ttaGpuErrorBox.querySelector('.retry-button').onclick = () => ttaButton.click();
ttaGpuErrorBox.querySelector('.back-button').onclick = () => { ttaGpuErrorBox.style.display = 'none'; };
// *** START CORRECTION 1 ***
ttaGpuErrorBox.querySelector('#tutorialLinkBtn').onclick = () => {
parent.postMessage({ type: 'NAVIGATE_TO_TUTORIAL' }, '*');
};
// *** END CORRECTION 1 ***
} else if (error.message && error.message.includes("No GPU was available")) {
ttaGpuErrorBox.innerHTML = GPU_TIMEOUT_ERROR_HTML;
ttaGpuErrorBox.style.display = 'block';
ttaGpuErrorBox.querySelector('.retry-button').onclick = () => ttaButton.click();
ttaGpuErrorBox.querySelector('.back-button').onclick = () => { ttaGpuErrorBox.style.display = 'none'; };
} else {
ttaResult.innerHTML = `<p class="status-text error">خطا در پردازش: ${error.message}</p>`;
}
} finally { ttaButton.disabled = false; }
});
// --- Video-to-Audio Logic (MODIFIED) ---
const vtaButton = document.getElementById('generate-video-audio');
const vtaResult = document.getElementById('vta-result');
const vtaGpuErrorBox = document.getElementById('vta-gpu-error-box');
vtaButton.addEventListener('click', async () => {
const videoFile = document.getElementById('vta-video').files[0];
const persianPrompt = document.getElementById('vta-prompt').value;
const persianNegativePrompt = document.getElementById('vta-negative-prompt').value;
if (!videoFile) { alert('لطفا یک فایل ویدیویی انتخاب کنید.'); return; }
vtaButton.disabled = true;
vtaGpuErrorBox.style.display = 'none';
vtaResult.innerHTML = '<div class="loader"></div><p class="status-text">۱. در حال ارسال و پردازش ویدیو...</p>';
try {
const [englishPrompt, englishNegativePrompt] = await Promise.all([ translateText(persianPrompt), translateText(persianNegativePrompt) ]);
vtaResult.innerHTML = `<div class="loader"></div><p class="status-text">۲. در حال آپلود ویدیو و پردازش صدا... (ممکن است طول بکشد)</p>`;
const app = await client("hkchengrex/MMAudio");
const result = await app.predict("/predict", [ { video: videoFile, subtitles: null }, englishPrompt, englishNegativePrompt, Number(document.getElementById('vta-seed').value), 25, 4.5, Number(document.getElementById('vta-duration').value) ]);
// --- MODIFIED SUCCESS BLOCK ---
vtaResult.innerHTML = ''; // Clear the loader
const successText = document.createElement('p');
successText.className = 'status-text success';
successText.textContent = 'ویدیوی جدید با موفقیت ایجاد شد!';
const videoPlayer = document.createElement('video');
videoPlayer.controls = true;
videoPlayer.src = result.data[0].video.url;
const downloadBtn = createDownloadButton(result.data[0].video.url);
vtaResult.appendChild(successText);
vtaResult.appendChild(videoPlayer);
vtaResult.appendChild(downloadBtn);
// --- END MODIFIED BLOCK ---
} catch (error) {
console.error("Video-to-Audio Error:", error);
vtaResult.innerHTML = '';
if (error.message && (error.message.includes("exceeded your GPU quota") || error.message.toLowerCase().includes("capacity"))) {
vtaGpuErrorBox.innerHTML = GPU_QUOTA_INSTRUCTIONS_HTML;
vtaGpuErrorBox.style.display = 'block';
// Add event listeners for the new buttons
vtaGpuErrorBox.querySelector('.retry-button').onclick = () => vtaButton.click();
vtaGpuErrorBox.querySelector('.back-button').onclick = () => { vtaGpuErrorBox.style.display = 'none'; };
// *** START CORRECTION 2 ***
vtaGpuErrorBox.querySelector('#tutorialLinkBtn').onclick = () => {
parent.postMessage({ type: 'NAVIGATE_TO_TUTORIAL' }, '*');
};
// *** END CORRECTION 2 ***
} else if (error.message && error.message.includes("No GPU was available")) {
vtaGpuErrorBox.innerHTML = GPU_TIMEOUT_ERROR_HTML;
vtaGpuErrorBox.style.display = 'block';
vtaGpuErrorBox.querySelector('.retry-button').onclick = () => vtaButton.click();
vtaGpuErrorBox.querySelector('.back-button').onclick = () => { vtaGpuErrorBox.style.display = 'none'; };
} else {
vtaResult.innerHTML = `<p class="status-text error">خطا در پردازش: ${error.message}</p>`;
}
} finally { vtaButton.disabled = false; }
});
</script>
</body>
</html>