Opera8 commited on
Commit
d5befac
·
verified ·
1 Parent(s): d6ee588

Create templates/vevo-voice.html

Browse files
Files changed (1) hide show
  1. templates/vevo-voice.html +351 -0
templates/vevo-voice.html ADDED
@@ -0,0 +1,351 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="fa" dir="rtl">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>استودیو تبدیل صدای Vevo آلفا</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
9
+ <style>
10
+ @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;700&display=swap');
11
+ body {
12
+ font-family: 'Vazirmatn', sans-serif;
13
+ }
14
+ .progress-slim { height: 4px; border-radius: 10px; background: #1e293b; overflow: hidden; margin-top: 8px; }
15
+ .progress-bar-anim {
16
+ height: 100%; width: 100%;
17
+ animation: shimmy 1.5s infinite linear;
18
+ background: linear-gradient(90deg, #3b82f6, #6366f1, #3b82f6);
19
+ background-size: 200% 100%;
20
+ }
21
+ @keyframes shimmy { 0%{background-position:100% 0} 100%{background-position:-100% 0} }
22
+ </style>
23
+ </head>
24
+ <body class="bg-slate-950 text-slate-100 min-h-screen flex flex-col">
25
+
26
+ <header class="border-b border-slate-800 bg-slate-900/50 py-4 px-6 mb-8">
27
+ <div class="max-w-7xl mx-auto flex flex-col md:flex-row justify-between items-center gap-4">
28
+ <div>
29
+ <h1 class="text-2xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-indigo-400">
30
+ سامانه تبدیل صدای Vevo (Opera8 / Sada)
31
+ </h1>
32
+ <p class="text-xs text-slate-400 mt-1 font-medium">اجرا و پردازش ایمن بر روی کلودهای توزیع‌شده گیت‌هاب اکشنز</p>
33
+ </div>
34
+ <div class="text-xs text-slate-500 bg-slate-900 px-3 py-1.5 rounded-md border border-slate-800">
35
+ GitHub Action Load-Balanced Client v2
36
+ </div>
37
+ </div>
38
+ </header>
39
+
40
+ <main class="flex-grow max-w-7xl w-full mx-auto px-4 md:px-6 pb-12">
41
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
42
+
43
+ <div class="lg:col-span-1 bg-slate-900 border border-slate-800 p-6 rounded-xl flex flex-col gap-5 h-fit">
44
+ <h2 class="text-lg font-semibold text-blue-400 border-b border-slate-800 pb-2">تنظیمات اتصال به سرویس</h2>
45
+
46
+ <div>
47
+ <label class="block text-xs text-slate-400 mb-2 font-medium">آدرس پایه سرور RVC (Gradio Space):</label>
48
+ <input type="text" id="baseUrl" value="https://opera8-sada.hf.space"
49
+ class="w-full bg-slate-950 border border-slate-800 rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-blue-500 transition-colors">
50
+ </div>
51
+
52
+ <div class="bg-slate-950 p-4 rounded-lg border border-slate-800">
53
+ <span class="block text-xs text-slate-400 mb-1 font-medium">سابقه کارهای من:</span>
54
+ <button onclick="confirmClearAll()" class="mt-2 text-xs text-red-400 hover:text-red-300 font-medium transition-colors">
55
+ <i class="fas fa-trash-alt me-1"></i> پاک‌سازی تاریخچه صوتی
56
+ </button>
57
+
58
+ <div id="historyList" class="mt-4 flex flex-col gap-3 max-h-96 overflow-y-auto">
59
+ <!-- کارهای ذخیره شده اینجا نمایش داده می‌شوند -->
60
+ </div>
61
+ </div>
62
+ </div>
63
+
64
+ <div class="lg:col-span-2 flex flex-col gap-6">
65
+ <div class="bg-slate-900 border border-slate-800 p-6 rounded-xl">
66
+ <h2 class="text-lg font-semibold text-blue-400 border-b border-slate-800 pb-2 mb-6">آپلود فایلهای ورودی</h2>
67
+
68
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
69
+ <div class="border border-dashed border-slate-700 hover:border-blue-500 p-4 rounded-lg bg-slate-950/50 flex flex-col items-center justify-center text-center transition-colors">
70
+ <span class="text-2xl mb-2">🎙️</span>
71
+ <span class="text-sm font-medium text-slate-200 mb-1">فایل صوتی اصلی (Source Audio)</span>
72
+ <input type="file" id="sourceAudio" accept="audio/*" class="text-xs text-slate-400 file:mr-4 file:py-1 file:px-3 file:rounded file:border-0 file:text-xs file:font-semibold file:bg-blue-600 file:text-white hover:file:bg-blue-700">
73
+ </div>
74
+
75
+ <div class="border border-dashed border-slate-700 hover:border-blue-500 p-4 rounded-lg bg-slate-950/50 flex flex-col items-center justify-center text-center transition-colors">
76
+ <span class="text-2xl mb-2">👤</span>
77
+ <span class="text-sm font-medium text-slate-200 mb-1">فایل صوتی مرجع (Reference Timbre)</span>
78
+ <input type="file" id="refAudio" accept="audio/*" class="text-xs text-slate-400 file:mr-4 file:py-1 file:px-3 file:rounded file:border-0 file:text-xs file:font-semibold file:bg-blue-600 file:text-white hover:file:bg-blue-700">
79
+ </div>
80
+ </div>
81
+
82
+ <div class="mt-8">
83
+ <button id="convertBtn" onclick="startVoiceConversion()" class="w-full py-3 bg-blue-600 hover:bg-blue-700 disabled:bg-slate-800 disabled:text-slate-500 disabled:cursor-not-allowed text-white rounded-lg font-bold text-sm shadow-lg shadow-blue-900/20 transition-all">
84
+ شروع فرآیند تبدیل صدا
85
+ </button>
86
+ </div>
87
+ </div>
88
+ </div>
89
+ </div>
90
+
91
+ <div class="mt-8 bg-slate-900 border border-slate-800 rounded-xl p-4 flex flex-col">
92
+ <div class="flex justify-between items-center border-b border-slate-800 pb-2 mb-3">
93
+ <h3 class="text-sm font-semibold text-slate-300">لاگ‌ها و گزارشات فنی (Developer Console)</h3>
94
+ <button onclick="clearLogs()" class="text-[10px] text-slate-500 hover:text-slate-300">پاک کردن لاگ‌ها</button>
95
+ </div>
96
+ <div id="consoleLogs" class="bg-slate-950 p-4 rounded-lg font-mono text-xs overflow-y-auto h-64 border border-slate-800/80 flex flex-col gap-1 leading-relaxed"></div>
97
+ </div>
98
+ </main>
99
+
100
+ <footer class="border-t border-slate-800 py-6 text-center text-xs text-slate-600 bg-slate-950">
101
+ <div class="max-w-7xl mx-auto px-4">
102
+ طراحی شده با سیستم توزیع پردازش بر روی ریپازیتوری‌های گیت‌هاب اکشنز.
103
+ </div>
104
+ </footer>
105
+
106
+ <script>
107
+ let pollIntervals = {};
108
+
109
+ function log(message, type = 'info') {
110
+ const consoleEl = document.getElementById('consoleLogs');
111
+ const logItem = document.createElement('div');
112
+ let colorClass = 'text-slate-300';
113
+ let prefix = '⚙️';
114
+
115
+ if (type === 'error') { colorClass = 'text-red-400 font-semibold'; prefix = '❌'; }
116
+ else if (type === 'success') { colorClass = 'text-emerald-400 font-semibold'; prefix = '✅'; }
117
+ else if (type === 'warning') { colorClass = 'text-amber-400'; prefix = '⚠️'; }
118
+ else if (type === 'debug') { colorClass = 'text-sky-400'; prefix = '🔍'; }
119
+
120
+ logItem.className = `py-1 border-b border-slate-900/50 ${colorClass}`;
121
+ logItem.innerHTML = `<span class="opacity-40 select-none">[${new Date().toLocaleTimeString()}]</span> <span class="mr-1">${prefix}</span> ${message}`;
122
+ consoleEl.appendChild(logItem);
123
+ consoleEl.scrollTop = consoleEl.scrollHeight;
124
+ }
125
+
126
+ function clearLogs() {
127
+ document.getElementById('consoleLogs').innerHTML = '';
128
+ log("کنسول پاکسازی شد.");
129
+ }
130
+
131
+ async function uploadFileToDocker(file, runId, suffix) {
132
+ const formData = new FormData();
133
+ const ext = file.name.split('.').pop().toLowerCase();
134
+ formData.append('run_id', `${runId}_${suffix}`);
135
+ formData.append('ext', ext);
136
+ formData.append('file', file);
137
+
138
+ const response = await fetch('/api/webhook/upload', { method: 'POST', body: formData });
139
+ if (!response.ok) throw new Error("خطا در آپلود صوتی ورودی به هاست اصلی.");
140
+ return ext;
141
+ }
142
+
143
+ function safeBase64Encode(str) {
144
+ return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, (match, p1) => {
145
+ return String.fromCharCode('0x' + p1);
146
+ }));
147
+ }
148
+
149
+ function getJobs() { return JSON.parse(localStorage.getItem('alpha_vevo_jobs_v1') || '[]'); }
150
+
151
+ function saveJob(job) {
152
+ const jobs = getJobs();
153
+ jobs.unshift(job);
154
+ localStorage.setItem('alpha_vevo_jobs_v1', JSON.stringify(jobs));
155
+ renderHistory();
156
+ }
157
+
158
+ function renameJobId(oldId, newId) {
159
+ const jobs = getJobs();
160
+ const idx = jobs.findIndex(j => j.id === oldId);
161
+ if (idx !== -1) {
162
+ jobs[idx].id = newId;
163
+ localStorage.setItem('alpha_vevo_jobs_v1', JSON.stringify(jobs));
164
+ }
165
+ }
166
+
167
+ function updateJobStatus(id, status, filename, logMsg) {
168
+ const jobs = getJobs(); const idx = jobs.findIndex(j => j.id === id);
169
+ if (idx !== -1) {
170
+ jobs[idx].status = status; if(filename) jobs[idx].filename = filename; if(logMsg) jobs[idx].log = logMsg;
171
+ localStorage.setItem('alpha_vevo_jobs_v1', JSON.stringify(jobs)); renderHistory();
172
+ }
173
+ }
174
+
175
+ function deleteJob(id) {
176
+ const jobs = getJobs().filter(j => j.id !== id);
177
+ localStorage.setItem('alpha_vevo_jobs_v1', JSON.stringify(jobs));
178
+ if (pollIntervals[id]) clearInterval(pollIntervals[id]);
179
+ renderHistory();
180
+ }
181
+
182
+ function confirmClearAll() {
183
+ if(confirm("تمام تاریخچه تبدیل صدای Vevo شما پاک شود؟")) {
184
+ Object.values(pollIntervals).forEach(clearInterval);
185
+ localStorage.removeItem('alpha_vevo_jobs_v1');
186
+ renderHistory();
187
+ log("تمام تاریخچه‌های صوتی پاک شدند.");
188
+ }
189
+ }
190
+
191
+ function renderHistory() {
192
+ const jobs = getJobs();
193
+ const list = document.getElementById('historyList');
194
+ list.innerHTML = '';
195
+
196
+ if (jobs.length === 0) {
197
+ list.innerHTML = `<div class="text-center py-4 text-slate-500 text-xs">تاریخچه خالی است.</div>`;
198
+ return;
199
+ }
200
+
201
+ jobs.forEach(job => {
202
+ let badge = '';
203
+ let content = '';
204
+
205
+ if (job.status === 'completed') {
206
+ badge = `<span class="text-[10px] bg-emerald-500/20 text-emerald-400 px-2 py-0.5 rounded font-bold">آماده</span>`;
207
+ content = `<div class="mt-2"><audio controls src="${job.filename}" class="w-full h-8 rounded bg-slate-900"></audio></div>`;
208
+ } else if (job.status === 'failed') {
209
+ badge = `<span class="text-[10px] bg-red-500/20 text-red-400 px-2 py-0.5 rounded font-bold">خطا</span>`;
210
+ content = `<div class="mt-1 text-[11px] text-red-400 bg-red-950/20 p-2 rounded border border-red-900/30">${job.log || 'خطا در گیت‌هاب'}</div>`;
211
+ } else {
212
+ badge = `<span class="text-[10px] bg-amber-500/20 text-amber-400 px-2 py-0.5 rounded font-bold">در حال پردازش</span>`;
213
+ content = `
214
+ <div class="mt-2 text-[11px] text-slate-400">
215
+ <div>${job.log || 'درحال انتقال...'}</div>
216
+ <div class="progress-slim"><div class="progress-bar-anim"></div></div>
217
+ </div>`;
218
+ }
219
+
220
+ const item = document.createElement('div');
221
+ item.className = "bg-slate-950 p-3 rounded-lg border border-slate-800/80 relative flex flex-col";
222
+ item.innerHTML = `
223
+ <button class="absolute top-2 left-2 text-slate-500 hover:text-red-400 text-xs" onclick="deleteJob('${job.id}')">&times;</button>
224
+ <div class="flex justify-between items-center mb-1">
225
+ <span class="text-xs font-semibold text-slate-200">پروژه #${job.id.substring(0, 6)}</span>
226
+ ${badge}
227
+ </div>
228
+ <span class="text-[10px] text-slate-500">${job.date}</span>
229
+ ${content}
230
+ `;
231
+ list.appendChild(item);
232
+ });
233
+ }
234
+
235
+ function startPolling(runId) {
236
+ if (pollIntervals[runId]) clearInterval(pollIntervals[runId]);
237
+
238
+ pollIntervals[runId] = setInterval(async () => {
239
+ try {
240
+ const statusRes = await fetch(`/api/status/${runId}`);
241
+ const statusData = await statusRes.json();
242
+
243
+ if (statusData.status === 'ready') {
244
+ clearInterval(pollIntervals[runId]);
245
+ updateJobStatus(runId, 'completed', statusData.url, 'تکمیل شد.');
246
+ log(`پردازش با موفقیت به پایان رسید. آدرس خروجی: ${statusData.url}`, "success");
247
+ } else if (statusData.status === 'failed') {
248
+ clearInterval(pollIntervals[runId]);
249
+ updateJobStatus(runId, 'failed', null, statusData.message || 'خطا در عملیات.');
250
+ log(`خطای سیستمی گیت‌هاب: ${statusData.message || 'Error'}`, "error");
251
+ } else {
252
+ const activeRes = await fetch('/api/actions/active');
253
+ const activeData = await activeRes.json();
254
+ const hasActiveActions = activeData.status === 'success' && activeData.actions.length > 0;
255
+
256
+ if (hasActiveActions) {
257
+ updateJobStatus(runId, 'processing', null, 'رانر گیت‌هاب در حال آماده‌سازی خط لوله Vevo و تبدیل فرکانس صدا...');
258
+ } else {
259
+ updateJobStatus(runId, 'processing', null, 'در انتظار تخصیص پردازشگر موازی در سرورهای گیت‌هاب...');
260
+ }
261
+ }
262
+ } catch (e) {
263
+ console.error("Error polling:", e);
264
+ }
265
+ }, 4000);
266
+ }
267
+
268
+ async function startVoiceConversion() {
269
+ const baseUrlRaw = document.getElementById('baseUrl').value.trim();
270
+ const sourceFileInput = document.getElementById('sourceAudio');
271
+ const refFileInput = document.getElementById('refAudio');
272
+ const convertBtn = document.getElementById('convertBtn');
273
+
274
+ if (!sourceFileInput.files[0] || !refFileInput.files[0]) {
275
+ alert("لطفاً هم فایل صوتی اصلی و هم مرجع را انتخاب کنید.");
276
+ return;
277
+ }
278
+
279
+ convertBtn.disabled = true;
280
+ convertBtn.textContent = "در حال انتقال فرآیند...";
281
+
282
+ const currentRunId = "vev" + Math.random().toString(36).substring(2, 14);
283
+
284
+ try {
285
+ saveJob({
286
+ id: currentRunId,
287
+ status: 'processing',
288
+ date: new Date().toLocaleTimeString('fa-IR'),
289
+ log: 'در حال آپلود صوتی به سرور اصلی...',
290
+ filename: null
291
+ });
292
+
293
+ log("شروع آپلود فایل صوتی اصلی...", "info");
294
+ const sourceExt = await uploadFileToDocker(sourceFileInput.files[0], currentRunId, 'vevo_source');
295
+ log("فایل صوتی اصلی با موفقیت به سرور داکر منتقل شد.", "success");
296
+
297
+ log("شروع آپلود فایل صوتی مرجع...", "info");
298
+ const refExt = await uploadFileToDocker(refFileInput.files[0], currentRunId, 'vevo_ref');
299
+ log("فایل صوتی مرجع با موفقیت به سرور داکر منتقل شد.", "success");
300
+
301
+ const b64SpaceUrl = safeBase64Encode(baseUrlRaw);
302
+ const vevoPayload = `VEVOCONFIG_userRunId_${currentRunId}_sourceExt_${sourceExt}_refExt_${refExt}_spaceUrl_${b64SpaceUrl}`;
303
+
304
+ log("تخصیص نوبت در صف پردازشی سرورهای گیت‌هاب اکشنز...", "info");
305
+ updateJobStatus(currentRunId, 'processing', null, 'ارسال درخواست به صف سرورهای موازی گیت‌هاب...');
306
+
307
+ const response = await fetch('/api/generate', {
308
+ method: 'POST',
309
+ headers: { 'Content-Type': 'application/json' },
310
+ body: JSON.stringify({
311
+ prompt: vevoPayload,
312
+ action_name: 'vevo-voice'
313
+ })
314
+ });
315
+
316
+ const data = await response.json();
317
+
318
+ if (data.status === 'success') {
319
+ renameJobId(currentRunId, data.run_id);
320
+ startPolling(data.run_id);
321
+ log(`درخواست به صورت رسمی با شناسه رهگیری ${data.run_id} در صف گیت‌هاب ثبت شد.`, "success");
322
+ sourceFileInput.value = "";
323
+ refFileInput.value = "";
324
+ } else {
325
+ throw new Error(data.message || 'خطا در ثبت نوبت صف.');
326
+ }
327
+
328
+ } catch (err) {
329
+ log(`بروز خطا در شروع فرآیند: ${err.message}`, "error");
330
+ updateJobStatus(currentRunId, 'failed', null, err.message || "خطا در شروع عملیات.");
331
+ } finally {
332
+ convertBtn.disabled = false;
333
+ convertBtn.textContent = "شروع فرآیند تبدیل صدا";
334
+ }
335
+ }
336
+
337
+ window.addEventListener('DOMContentLoaded', () => {
338
+ log("کلاینت وب لود شد.");
339
+ renderHistory();
340
+
341
+ // بازگردانی مانیتورینگ کارها در صورت رفرش تصادفی صفحه
342
+ const jobs = getJobs();
343
+ jobs.forEach(job => {
344
+ if (job.status === 'processing') {
345
+ startPolling(job.id);
346
+ }
347
+ });
348
+ });
349
+ </script>
350
+ </body>
351
+ </html>