| <!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> |
| :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); |
| } |
| |
| * { 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 { 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 { 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 { 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); } |
| |
| .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); } |
| |
| .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); } |
| |
| .download-btn { display: inline-flex; align-items: center; justify-content: center; gap: 10px; padding: 12px 25px; margin-top: 20px; width: auto; 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); } |
| |
| .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; } |
| 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; } |
| |
| |
| #toast-container { |
| position: fixed; |
| top: 20px; |
| left: 50%; |
| transform: translateX(-50%); |
| z-index: 9999; |
| display: flex; |
| flex-direction: column; |
| gap: 10px; |
| } |
| .toast { |
| background: linear-gradient(135deg, #ff0844 0%, #ffb199 100%); |
| color: white; |
| padding: 15px 25px; |
| border-radius: 50px; |
| font-family: var(--app-font); |
| font-size: 1rem; |
| font-weight: bold; |
| box-shadow: 0 10px 25px rgba(255, 8, 68, 0.4); |
| display: flex; |
| align-items: center; |
| gap: 10px; |
| animation: slideDownBounce 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55) forwards; |
| opacity: 0; |
| } |
| .toast.fade-out { |
| animation: fadeOutUp 0.5s forwards; |
| } |
| @keyframes slideDownBounce { |
| 0% { transform: translateY(-50px) scale(0.8); opacity: 0; } |
| 100% { transform: translateY(0) scale(1); opacity: 1; } |
| } |
| @keyframes fadeOutUp { |
| to { opacity: 0; transform: translateY(-50px) scale(0.9); } |
| } |
| </style> |
| </head> |
| <body> |
| <div id="toast-container"></div> |
|
|
| <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" max="60" min="1"> |
| </div> |
| </div> |
| <div id="vta-result" class="result"></div> |
| <button id="generate-video-audio" class="btn"><span class="icon">🪄</span>ایجاد صدا برای ویدیو</button> |
| </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" max="60" min="1"> |
| </div> |
| </div> |
| <div id="tta-result" class="result"></div> |
| <button id="generate-text-audio" class="btn"><span class="icon">✨</span>ایجاد صدا</button> |
| </div> |
| </div> |
|
|
| <script> |
| |
| function showToast(message) { |
| const container = document.getElementById('toast-container'); |
| const toast = document.createElement('div'); |
| toast.className = 'toast'; |
| toast.innerHTML = `<span>⚠️</span> ${message}`; |
| container.appendChild(toast); |
| |
| setTimeout(() => { |
| toast.classList.add('fade-out'); |
| setTimeout(() => toast.remove(), 500); |
| }, 4000); |
| } |
| |
| const tabs = document.querySelectorAll('.tab'); |
| const tabContents = document.querySelectorAll('.tab-content'); |
| |
| 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'); |
| }); |
| }); |
| |
| 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 videoElement = document.createElement('video'); |
| videoElement.preload = 'metadata'; |
| videoElement.onloadedmetadata = function() { |
| window.URL.revokeObjectURL(videoElement.src); |
| |
| if (videoElement.duration > 60) { |
| showToast('ویدیوی انتخابی طولانی است! حداکثر زمان مجاز ۱ دقیقه است.'); |
| vtaFileInput.value = ''; |
| vtaUploadContent.style.display = 'flex'; |
| vtaFileUploadArea.classList.remove('has-preview'); |
| return; |
| } |
| |
| |
| 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'); |
| }; |
| videoElement.src = URL.createObjectURL(file); |
| } else { |
| vtaUploadContent.style.display = 'flex'; |
| vtaFileUploadArea.classList.remove('has-preview'); |
| } |
| }; |
| |
| vtaFileInput.addEventListener('change', handleFileSelect); |
| |
| function createDownloadButton(fileUrl) { |
| const button = document.createElement('button'); |
| button.className = 'download-btn'; |
| button.innerHTML = `<span class="icon">⬇️</span> دانلود`; |
| button.onclick = () => { |
| parent.postMessage({ type: 'DOWNLOAD_REQUEST', url: fileUrl }, '*'); |
| }; |
| return button; |
| } |
| |
| async function pollStatus(runId, resultContainer) { |
| const interval = setInterval(async () => { |
| try { |
| const res = await fetch(`/api/status/${runId}`); |
| const data = await res.json(); |
| if (data.status === 'ready') { |
| clearInterval(interval); |
| resultContainer.innerHTML = ''; |
| const successText = document.createElement('p'); |
| successText.className = 'status-text success'; |
| successText.textContent = 'صدا با موفقیت ایجاد شد!'; |
| |
| const media = document.createElement('video'); |
| media.controls = true; |
| media.src = data.url; |
| media.style.maxHeight = '250px'; |
| |
| const downloadBtn = createDownloadButton(data.url); |
| |
| resultContainer.appendChild(successText); |
| resultContainer.appendChild(media); |
| resultContainer.appendChild(downloadBtn); |
| } |
| } catch (e) { |
| console.error('Polling error:', e); |
| } |
| }, 3000); |
| } |
| |
| const ttaButton = document.getElementById('generate-text-audio'); |
| const ttaResult = document.getElementById('tta-result'); |
| ttaButton.addEventListener('click', async () => { |
| const prompt = document.getElementById('tta-prompt').value; |
| const durationVal = parseInt(document.getElementById('tta-duration').value); |
| |
| if (!prompt) { alert('لطفا متن اصلی را وارد کنید.'); return; } |
| if (durationVal > 60) { |
| showToast('زمان تولید نمیتواند بیشتر از ۶۰ ثانیه باشد!'); |
| return; |
| } |
| |
| ttaButton.disabled = true; |
| ttaResult.innerHTML = '<div class="loader"></div><p class="status-text">۱. در حال ارسال دستور پردازش...</p>'; |
| |
| const formData = new FormData(); |
| formData.append('type', 'text'); |
| formData.append('prompt', prompt); |
| formData.append('negative_prompt', document.getElementById('tta-negative-prompt').value); |
| formData.append('seed', document.getElementById('tta-seed').value); |
| formData.append('duration', durationVal); |
| |
| try { |
| const response = await fetch('/api/generate-audio', { method: 'POST', body: formData }); |
| const data = await response.json(); |
| if (data.status === 'success') { |
| ttaResult.innerHTML = `<div class="loader"></div><p class="status-text">۲. دستور با موفقیت ارسال شد.<br>در حال تولید صدای درخواستی، تولید صدا ممکنه زمان بر باشه لطفاً صبور باشید...</p>`; |
| pollStatus(data.run_id, ttaResult); |
| } else { |
| ttaResult.innerHTML = `<p class="status-text error">خطا: ${data.message}</p>`; |
| } |
| } catch (error) { |
| ttaResult.innerHTML = `<p class="status-text error">خطای ارتباط: ${error.message}</p>`; |
| } finally { |
| ttaButton.disabled = false; |
| } |
| }); |
| |
| const vtaButton = document.getElementById('generate-video-audio'); |
| const vtaResult = document.getElementById('vta-result'); |
| vtaButton.addEventListener('click', async () => { |
| const videoFile = document.getElementById('vta-video').files[0]; |
| const durationVal = parseInt(document.getElementById('vta-duration').value); |
| |
| if (!videoFile) { alert('لطفا یک فایل ویدیویی انتخاب کنید.'); return; } |
| if (durationVal > 60) { |
| showToast('زمان تولید نمیتواند بیشتر از ۶۰ ثانیه باشد!'); |
| return; |
| } |
| |
| vtaButton.disabled = true; |
| vtaResult.innerHTML = '<div class="loader"></div><p class="status-text">۱. در حال ارسال ویدیو و دستورات پردازش...</p>'; |
| |
| const formData = new FormData(); |
| formData.append('type', 'video'); |
| formData.append('file', videoFile); |
| formData.append('prompt', document.getElementById('vta-prompt').value); |
| formData.append('negative_prompt', document.getElementById('vta-negative-prompt').value); |
| formData.append('seed', document.getElementById('vta-seed').value); |
| formData.append('duration', durationVal); |
| |
| try { |
| const response = await fetch('/api/generate-audio', { method: 'POST', body: formData }); |
| const data = await response.json(); |
| if (data.status === 'success') { |
| vtaResult.innerHTML = `<div class="loader"></div><p class="status-text">۲. فایل ویدیویی ارسال شد.<br>در حال ساخت صدای سینمایی منطبق بر تصویر... (ساخت صدا ممکنه زمان بر باشه لطفاً صبور باشید)</p>`; |
| pollStatus(data.run_id, vtaResult); |
| } else { |
| vtaResult.innerHTML = `<p class="status-text error">خطا: ${data.message}</p>`; |
| } |
| } catch (error) { |
| vtaResult.innerHTML = `<p class="status-text error">خطای ارتباط: ${error.message}</p>`; |
| } finally { |
| vtaButton.disabled = false; |
| } |
| }); |
| </script> |
| </body> |
| </html> |