Spaces:
Running
Running
| <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 ; border: none ; background: transparent ; } | |
| .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 را خاموش کرده و طبق ویدیو آموزشی پایین نقطه دستیابی رو تغییر دهید. «تلاش مجدد» کلیک کنید. با این روش ساده میتوانید به صورت نامحدود صدا بسازید! ☘️</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 = '×'; | |
| 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> |