| <!DOCTYPE html> |
| <html lang="fa" dir="rtl"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>پنل اختصاصی تولید موسیقی ACE-Step V1.5</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;700&display=swap'); |
| body { |
| font-family: 'Vazirmatn', sans-serif; |
| background: linear-gradient(135deg, #0f172a 0%, #1e1b4b 100%); |
| } |
| .glass-panel { |
| background: rgba(30, 41, 59, 0.7); |
| backdrop-filter: blur(12px); |
| -webkit-backdrop-filter: blur(12px); |
| border: 1px solid rgba(255, 255, 255, 0.1); |
| } |
| .progress-slim { height: 4px; border-radius: 10px; background: #1e293b; overflow: hidden; margin-top: 8px; } |
| .progress-bar-anim { |
| height: 100%; width: 100%; |
| animation: shimmy 1.5s infinite linear; |
| background: linear-gradient(90deg, #8b5cf6, #d946ef, #8b5cf6); |
| background-size: 200% 100%; |
| } |
| @keyframes shimmy { 0%{background-position:100% 0} 100%{background-position:-100% 0} } |
| </style> |
| </head> |
| <body class="min-h-screen text-slate-100 p-4 md:p-8 flex flex-col justify-between"> |
|
|
| <div class="max-w-6xl mx-auto w-full"> |
| <header class="text-center mb-8"> |
| <h1 class="text-3xl md:text-5xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-violet-400 to-fuchsia-400 mb-2"> |
| 🎛️ کلاینت هوشمند ACE-Step V1.5 💡 |
| </h1> |
| <p class="text-slate-400 text-sm md:text-base">پردازش و رندر ایمن و توزیعشده بر روی سرورهای ابری گیتهاب اکشنز</p> |
| </header> |
|
|
| <div class="grid grid-cols-1 lg:grid-cols-12 gap-8"> |
| <div class="lg:col-span-7 space-y-6"> |
| |
| |
| <div class="glass-panel p-6 rounded-2xl shadow-xl space-y-4"> |
| <h2 class="text-xl font-bold text-violet-300 border-b border-slate-700 pb-2 mb-4"> |
| <i class="fa-solid fa-sliders ml-2"></i>تنظیمات موسیقی |
| </h2> |
|
|
| <div> |
| <label class="block text-sm font-semibold mb-1 text-slate-300" for="caption">توصیف سبک موسیقی (Prompt):</label> |
| <textarea id="caption" rows="3" class="w-full bg-slate-900 border border-slate-700 rounded-lg p-3 text-sm focus:outline-none focus:border-violet-500 transition" placeholder="مثال: warm acoustic guitar, slow tempo, high quality, studio mastering..."></textarea> |
| </div> |
|
|
| <div> |
| <label class="block text-sm font-semibold mb-1 text-slate-300" for="lyrics">شعر یا متن لیریک (Lyrics):</label> |
| <textarea id="lyrics" rows="5" class="w-full bg-slate-900 border border-slate-700 rounded-lg p-3 text-sm focus:outline-none focus:border-violet-500 transition" placeholder="مثال: [Verse 1] تو مثل بارونی رو تن صحرا..."></textarea> |
| </div> |
|
|
| <div class="grid grid-cols-2 gap-4"> |
| <div class="flex flex-col space-y-2 justify-center bg-slate-900/50 p-3 rounded-lg border border-slate-800"> |
| <label class="flex items-center space-x-2 space-x-reverse cursor-pointer"> |
| <input type="checkbox" id="think_checkbox" class="w-4 h-4 text-violet-600 bg-slate-800 border-slate-700 rounded focus:ring-violet-500" checked> |
| <span class="text-sm font-medium mr-2">تفکر هوش مصنوعی (Thinking)</span> |
| </label> |
| <label class="flex items-center space-x-2 space-x-reverse cursor-pointer"> |
| <input type="checkbox" id="instrumental_checkbox" class="w-4 h-4 text-violet-600 bg-slate-800 border-slate-700 rounded focus:ring-violet-500"> |
| <span class="text-sm font-medium mr-2">بیکلام (Instrumental)</span> |
| </label> |
| </div> |
|
|
| <div class="space-y-3 bg-slate-900/50 p-3 rounded-lg border border-slate-800"> |
| <div> |
| <label class="block text-xs font-semibold text-slate-400 mb-1">زبان خواننده:</label> |
| <select id="vocal_language" class="w-full bg-slate-800 border border-slate-700 rounded-md p-1.5 text-xs"> |
| <option value="fa" selected>فارسی (fa)</option> |
| <option value="en">انگلیسی (en)</option> |
| <option value="ar">عربی (ar)</option> |
| <option value="tr">ترکی (tr)</option> |
| <option value="unknown">نامشخص/بیکلام (unknown)</option> |
| </select> |
| </div> |
| <div> |
| <label class="block text-xs font-semibold text-slate-400 mb-1">تعداد تولید همزمان (Batch Size):</label> |
| <select id="batch_size" class="w-full bg-slate-800 border border-slate-700 rounded-md p-1.5 text-xs"> |
| <option value="1" selected>۱ آهنگ</option> |
| <option value="2">۲ آهنگ</option> |
| </select> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="glass-panel p-6 rounded-2xl shadow-xl space-y-4"> |
| <h2 class="text-xl font-bold text-violet-300 border-b border-slate-700 pb-2"> |
| <i class="fa-solid fa-music ml-2"></i>آپلود فایل مرجع (اختیاری) |
| </h2> |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> |
| <div> |
| <label class="block text-xs font-semibold text-slate-400 mb-1">صدا یا سبک مرجع (Reference Audio):</label> |
| <input type="file" id="reference_audio" accept="audio/*" class="w-full text-xs text-slate-400 file:mr-0 file:ml-4 file:py-1.5 file:px-3 file:rounded-md file:border-0 file:text-xs file:font-semibold file:bg-violet-600 file:text-white hover:file:bg-violet-700 cursor-pointer bg-slate-900 p-2 rounded-lg border border-slate-700"> |
| </div> |
| <div> |
| <label class="block text-xs font-semibold text-slate-400 mb-1">آهنگ مبدا جهت بازخوانی (Source Audio):</label> |
| <input type="file" id="src_audio" accept="audio/*" class="w-full text-xs text-slate-400 file:mr-0 file:ml-4 file:py-1.5 file:px-3 file:rounded-md file:border-0 file:text-xs file:font-semibold file:bg-violet-600 file:text-white hover:file:bg-violet-700 cursor-pointer bg-slate-900 p-2 rounded-lg border border-slate-700"> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="glass-panel p-6 rounded-2xl shadow-xl space-y-4"> |
| <button onclick="toggleAdvanced()" class="w-full text-right flex justify-between items-center text-sm font-bold text-slate-400 hover:text-slate-100 transition"> |
| <span><i class="fa-solid fa-gears ml-2"></i>تنظیمات پیشرفته و ادامه تولید (Save & Resume)</span> |
| <i id="advanced_chevron" class="fa-solid fa-chevron-down transition-transform"></i> |
| </button> |
| |
| <div id="advanced_section" class="hidden space-y-4 pt-2 border-t border-slate-800"> |
| <div class="bg-violet-950/20 p-4 rounded-xl border border-violet-500/30"> |
| <label class="block text-sm font-bold text-violet-300 mb-1">کد پیگیری (Tracking ID) جهت ادامه تولید:</label> |
| <input type="text" id="tracking_id" class="w-full bg-slate-900 border border-slate-700 rounded-lg p-2.5 text-sm uppercase tracking-widest text-center text-violet-200" placeholder="مثال: DXAVSN"> |
| </div> |
|
|
| <div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-xs"> |
| <div> |
| <label class="block text-slate-400 mb-1">تمپو (BPM):</label> |
| <input type="number" id="bpm" class="w-full bg-slate-900 border border-slate-700 rounded-md p-1.5" placeholder="BPM"> |
| </div> |
| <div> |
| <label class="block text-slate-400 mb-1">کیاسکیل / گام:</label> |
| <input type="text" id="keyscale" class="w-full bg-slate-900 border border-slate-700 rounded-md p-1.5" placeholder="گام"> |
| </div> |
| <div> |
| <label class="block text-slate-400 mb-1">میزاننما:</label> |
| <input type="text" id="timesignature" class="w-full bg-slate-900 border border-slate-700 rounded-md p-1.5" placeholder="کسر میزان"> |
| </div> |
| <div> |
| <label class="block text-slate-400 mb-1">مدت زمان (ثانیه):</label> |
| <input type="number" id="duration" class="w-full bg-slate-900 border border-slate-700 rounded-md p-1.5" value="-1"> |
| </div> |
| </div> |
|
|
| <div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-xs"> |
| <div> |
| <label class="block text-slate-400 mb-1">تعداد مراحل:</label> |
| <input type="number" id="inference_steps" class="w-full bg-slate-900 border border-slate-700 rounded-md p-1.5" value="12"> |
| </div> |
| <div> |
| <label class="block text-slate-400 mb-1">سید (Seed):</label> |
| <input type="text" id="seed_input" class="w-full bg-slate-900 border border-slate-700 rounded-md p-1.5" value="-1"> |
| </div> |
| <div> |
| <label class="block text-slate-400 mb-1">فرمت فایل نهایی:</label> |
| <select id="audio_format" class="w-full bg-slate-900 border border-slate-700 rounded-md p-1.5"> |
| <option value="mp3" selected>MP3</option> |
| <option value="flac">FLAC</option> |
| </select> |
| </div> |
| <div> |
| <label class="block text-slate-400 mb-1">نوع کار (Task Type):</label> |
| <select id="task_type" class="w-full bg-slate-900 border border-slate-700 rounded-md p-1.5"> |
| <option value="text2music" selected>text2music</option> |
| <option value="cover">cover</option> |
| <option value="repaint">repaint</option> |
| </select> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <button id="generate_btn" onclick="startGeneration()" class="w-full py-4 px-6 rounded-2xl text-white font-extrabold text-lg bg-gradient-to-r from-violet-600 to-fuchsia-600 hover:from-violet-500 hover:to-fuchsia-500 shadow-lg shadow-violet-700/20 active:scale-95 transition transform flex justify-center items-center gap-3"> |
| <i class="fa-solid fa-music"></i> |
| <span>شروع فرآیند آهنگسازی در گیتهاب</span> |
| </button> |
|
|
| </div> |
|
|
| <div class="lg:col-span-5 flex flex-col space-y-6"> |
| |
| <div class="glass-panel p-6 rounded-2xl shadow-xl flex-1 flex flex-col justify-between"> |
| <div> |
| <div class="flex justify-between items-center border-b border-slate-700 pb-3 mb-4"> |
| <h2 class="text-xl font-bold text-fuchsia-300"> |
| <i class="fa-solid fa-history ml-2"></i>پروژههای من |
| </h2> |
| <button onclick="clearHistory()" class="text-xs text-red-400 hover:text-red-300 font-bold transition"> |
| <i class="fa-solid fa-trash-alt"></i> پاکسازی تاریخچه |
| </button> |
| </div> |
| |
| <div id="history_list" class="space-y-4 max-h-[420px] overflow-y-auto"> |
| |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <footer class="text-center text-xs text-slate-500 py-6 mt-8 border-t border-slate-900 max-w-6xl mx-auto w-full"> |
| ACE-Step Music Action Client © 2026 | اتصال توزیع شده به گیتهاب اکشنز جهت محاسبات نامحدود |
| </footer> |
|
|
| <script> |
| let pollIntervals = {}; |
| |
| function toggleAdvanced() { |
| const advancedSection = document.getElementById("advanced_section"); |
| const chevron = document.getElementById("advanced_chevron"); |
| advancedSection.classList.toggle("hidden"); |
| chevron.classList.toggle("rotate-180"); |
| } |
| |
| function getJobs() { return JSON.parse(localStorage.getItem('ace_music_jobs_v1') || '[]'); } |
| |
| function saveJob(job) { |
| const jobs = getJobs(); |
| jobs.unshift(job); |
| localStorage.setItem('ace_music_jobs_v1', JSON.stringify(jobs)); |
| renderHistory(); |
| } |
| |
| function renameJobId(oldId, newId) { |
| const jobs = getJobs(); |
| const idx = jobs.findIndex(j => j.id === oldId); |
| if (idx !== -1) { |
| jobs[idx].id = newId; |
| localStorage.setItem('ace_music_jobs_v1', JSON.stringify(jobs)); |
| } |
| } |
| |
| function updateJobStatus(id, status, filename, logMsg) { |
| const jobs = getJobs(); |
| const idx = jobs.findIndex(j => j.id === id); |
| if (idx !== -1) { |
| jobs[idx].status = status; |
| if(filename) jobs[idx].filename = filename; |
| if(logMsg) jobs[idx].log = logMsg; |
| localStorage.setItem('ace_music_jobs_v1', JSON.stringify(jobs)); |
| renderHistory(); |
| } |
| } |
| |
| function deleteJob(id) { |
| const jobs = getJobs().filter(j => j.id !== id); |
| localStorage.setItem('ace_music_jobs_v1', JSON.stringify(jobs)); |
| if (pollIntervals[id]) clearInterval(pollIntervals[id]); |
| renderHistory(); |
| } |
| |
| function clearHistory() { |
| if(confirm("تمام پروژههای موسیقی شما پاک شود؟")) { |
| Object.values(pollIntervals).forEach(clearInterval); |
| localStorage.removeItem('ace_music_jobs_v1'); |
| renderHistory(); |
| } |
| } |
| |
| function renderHistory() { |
| const jobs = getJobs(); |
| const list = document.getElementById('history_list'); |
| list.innerHTML = ''; |
| |
| if (jobs.length === 0) { |
| list.innerHTML = `<div class="text-center py-8 text-slate-500 text-sm">لیست پروژههای تولید شده خالی است.</div>`; |
| return; |
| } |
| |
| jobs.forEach(job => { |
| let badge = ''; |
| let content = ''; |
| |
| if (job.status === 'completed') { |
| badge = `<span class="text-xs bg-emerald-500/20 text-emerald-400 px-2.5 py-0.5 rounded font-bold">رندر شد</span>`; |
| const batch2Url = job.filename.includes("_batch2") ? "" : job.filename.replace(/\.(mp3|flac|wav)$/, "_batch2.$1"); |
| |
| content = ` |
| <div class="mt-2 space-y-2"> |
| <div class="text-xs font-semibold text-violet-400">قطعه شماره ۱:</div> |
| <audio controls src="${job.filename}" class="w-full h-8 rounded bg-slate-900"></audio> |
| <div class="flex justify-end pt-1"> |
| <a href="${job.filename}" download class="text-xs text-sky-400 hover:underline"><i class="fa-solid fa-download mr-1"></i>دانلود ترک اصلی</a> |
| </div> |
| `; |
| |
| if (job.batch_size == 2) { |
| content += ` |
| <div class="text-xs font-semibold text-fuchsia-400 mt-2">قطعه شماره ۲:</div> |
| <audio controls src="${batch2Url}" class="w-full h-8 rounded bg-slate-900" onerror="this.style.display='none'"></audio> |
| <div class="flex justify-end"> |
| <a href="${batch2Url}" download class="text-xs text-fuchsia-400 hover:underline" onerror="this.style.display='none'"><i class="fa-solid fa-download mr-1"></i>دانلود ترک دوم</a> |
| </div> |
| `; |
| } |
| content += `</div>`; |
| } else if (job.status === 'failed') { |
| badge = `<span class="text-xs bg-red-500/20 text-red-400 px-2.5 py-0.5 rounded font-bold">ناموفق</span>`; |
| content = `<div class="mt-2 text-xs text-red-400 bg-red-950/20 p-3 rounded border border-red-900/30">${job.log || 'خطا در رانر گیتهاب'}</div>`; |
| } else { |
| badge = `<span class="text-xs bg-amber-500/20 text-amber-400 px-2.5 py-0.5 rounded font-bold">در حال پردازش</span>`; |
| content = ` |
| <div class="mt-2 text-xs text-slate-400"> |
| <div>${job.log || 'دریافت صف...'}</div> |
| <div class="progress-slim"><div class="progress-bar-anim"></div></div> |
| </div>`; |
| } |
| |
| const item = document.createElement('div'); |
| item.className = "bg-slate-950 p-4 rounded-xl border border-slate-800 relative flex flex-col"; |
| item.innerHTML = ` |
| <button class="absolute top-3 left-3 text-slate-500 hover:text-red-400 text-lg transition" onclick="deleteJob('${job.id}')">×</button> |
| <div class="flex justify-between items-center mb-1"> |
| <span class="text-sm font-bold text-slate-200">پروژه #${job.id.substring(0, 6)}</span> |
| ${badge} |
| </div> |
| <span class="text-[10px] text-slate-500">${job.date}</span> |
| ${content} |
| `; |
| list.appendChild(item); |
| }); |
| } |
| |
| async function uploadFileToDocker(file, runId, suffix) { |
| const formData = new FormData(); |
| const ext = file.name.split('.').pop().toLowerCase(); |
| formData.append('run_id', `${runId}_music_${suffix}`); |
| formData.append('ext', ext); |
| formData.append('file', file); |
| |
| const response = await fetch('/api/webhook/upload', { method: 'POST', body: formData }); |
| if (!response.ok) throw new Error("خطا در آپلود فایل به هاست واسط."); |
| return ext; |
| } |
| |
| function safeBase64Encode(str) { |
| return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, (match, p1) => { |
| return String.fromCharCode('0x' + p1); |
| })); |
| } |
| |
| function startPolling(runId, batchSize = 1) { |
| if (pollIntervals[runId]) clearInterval(pollIntervals[runId]); |
| |
| pollIntervals[runId] = setInterval(async () => { |
| try { |
| const statusRes = await fetch(`/api/status/${runId}`); |
| const statusData = await statusRes.json(); |
| |
| if (statusData.status === 'ready') { |
| clearInterval(pollIntervals[runId]); |
| updateJobStatus(runId, 'completed', statusData.url, 'تولید موسیقی با موفقیت تکمیل شد.'); |
| } else if (statusData.status === 'failed') { |
| clearInterval(pollIntervals[runId]); |
| updateJobStatus(runId, 'failed', null, statusData.message || 'خطا در کامپایل نهایی.'); |
| } else { |
| const activeRes = await fetch('/api/actions/active'); |
| const activeData = await activeRes.json(); |
| const hasActiveActions = activeData.status === 'success' && activeData.actions.length > 0; |
| |
| if (hasActiveActions) { |
| updateJobStatus(runId, 'processing', null, 'رانر فعال شد. مدل در حال تحلیل لیریک و تولید موزیک در کانتینر ZeroGPU است...'); |
| } else { |
| updateJobStatus(runId, 'processing', null, 'در صف رزرو پردازشگر ابری... (اگر رانر آفلاین باشد، تا ۲۰ تلاش پایتون ادامه مییابد)'); |
| } |
| } |
| } catch (e) { |
| console.error("Error polling:", e); |
| } |
| }, 5000); |
| } |
| |
| async function startGeneration() { |
| const btn = document.getElementById("generate_btn"); |
| const refFileInput = document.getElementById("reference_audio"); |
| const srcFileInput = document.getElementById("src_audio"); |
| |
| btn.disabled = true; |
| btn.classList.add("opacity-50", "pointer-events-none"); |
| |
| const currentRunId = "mus" + Math.random().toString(36).substring(2, 14); |
| const batchSize = Number(document.getElementById("batch_size").value); |
| |
| try { |
| saveJob({ |
| id: currentRunId, |
| status: 'processing', |
| date: new Date().toLocaleTimeString('fa-IR'), |
| log: 'در حال بستهبندی فایلها و ارسال به هاست واسط...', |
| filename: null, |
| batch_size: batchSize |
| }); |
| |
| let refExt = null; |
| let srcExt = null; |
| |
| if (refFileInput.files[0]) { |
| refExt = await uploadFileToDocker(refFileInput.files[0], currentRunId, 'ref'); |
| } |
| if (srcFileInput.files[0]) { |
| srcExt = await uploadFileToDocker(srcFileInput.files[0], currentRunId, 'src'); |
| } |
| |
| const musicPayload = { |
| userRunId: currentRunId, |
| caption: document.getElementById("caption").value, |
| lyrics: document.getElementById("lyrics").value, |
| think_checkbox: document.getElementById("think_checkbox").checked, |
| instrumental_checkbox: document.getElementById("instrumental_checkbox").checked, |
| vocal_language: document.getElementById("vocal_language").value, |
| batch_size: batchSize, |
| bpm: document.getElementById("bpm").value, |
| keyscale: document.getElementById("keyscale").value, |
| timesignature: document.getElementById("timesignature").value, |
| duration: document.getElementById("duration").value, |
| inference_steps: document.getElementById("inference_steps").value, |
| seed_input: document.getElementById("seed_input").value, |
| audio_format: document.getElementById("audio_format").value, |
| task_type: document.getElementById("task_type").value, |
| tracking_id: document.getElementById("tracking_id").value, |
| ref_audio_ext: refExt, |
| src_audio_ext: srcExt |
| }; |
| |
| const b64Payload = safeBase64Encode(JSON.stringify(musicPayload)); |
| const finalPrompt = `VOICECONFIG_MUSIC_${b64Payload}`; |
| |
| updateJobStatus(currentRunId, 'processing', null, 'در حال راهاندازی فرآیند در صف موازی گیتهاب...'); |
| |
| const response = await fetch('/api/generate', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ |
| prompt: finalPrompt, |
| action_name: 'music' |
| }) |
| }); |
| |
| const data = await response.json(); |
| |
| if (data.status === 'success') { |
| renameJobId(currentRunId, data.run_id); |
| startPolling(data.run_id, batchSize); |
| |
| refFileInput.value = ""; |
| srcFileInput.value = ""; |
| } else { |
| throw new Error(data.message || 'ثبت نوبت در گیتهاب ناموفق بود.'); |
| } |
| |
| } catch (err) { |
| updateJobStatus(currentRunId, 'failed', null, err.message || "خطا در شروع عملیات."); |
| } finally { |
| btn.disabled = false; |
| btn.classList.remove("opacity-50", "pointer-events-none"); |
| } |
| } |
| |
| window.addEventListener('DOMContentLoaded', () => { |
| renderHistory(); |
| |
| getJobs().forEach(job => { |
| if (job.status === 'processing') { |
| startPolling(job.id, job.batch_size || 1); |
| } |
| }); |
| }); |
| </script> |
| </body> |
| </html> |