Spaces:
Running
Running
| import { JobData, StatusResponse, Model } from './types'; | |
| import * as legacyApi from './legacy_api'; | |
| const TTS_BASE_URL = 'https://sada8888-tts2.hf.space'; | |
| const safeBase64Encode = (str: string): string => { | |
| return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, (match, p1) => { | |
| return String.fromCharCode(parseInt('0x' + p1)); | |
| })); | |
| }; | |
| const uploadAudioToOrchestrator = async (file: Blob | File, runId: string, suffix: string): Promise<string> => { | |
| const formData = new FormData(); | |
| const ext = (file as File).name?.split('.').pop()?.toLowerCase() || 'wav'; | |
| formData.append('run_id', `${runId}_${suffix}`); | |
| formData.append('ext', ext); | |
| formData.append('file', file); | |
| const response = await fetch('https://opera8-action.hf.space/api/webhook/upload', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| if (!response.ok) throw new Error("خطا در آپلود فایل صوتی به سرور پشتیبان."); | |
| return ext; | |
| }; | |
| export const uploadJob = async ( | |
| sourceFile: File | Blob, | |
| refFile: File | Blob, | |
| metadata: { type?: 'custom' | 'model', modelName?: string, modelImage?: string } | |
| ): Promise<JobData> => { | |
| const currentRunId = "vev" + Math.random().toString(36).substring(2, 14); | |
| // ۱. آپلود صوتیهای مبدا و مرجع به ارکستریتور ابری | |
| const sourceExt = await uploadAudioToOrchestrator(sourceFile, currentRunId, 'vevo_source'); | |
| const refExt = await uploadAudioToOrchestrator(refFile, currentRunId, 'vevo_ref'); | |
| // ۲. رمزگذاری آدرس فضای پردازش و ساخت کلید تنظیمات Vevo | |
| const b64SpaceUrl = safeBase64Encode("https://opera8-sada.hf.space"); | |
| const vevoPayload = `VEVOCONFIG_userRunId_${currentRunId}_sourceExt_${sourceExt}_refExt_${refExt}_spaceUrl_${b64SpaceUrl}`; | |
| // ۳. ارسال دستور کار به سیستم دیسپچ گیتهاب اکشنز ابری | |
| const response = await fetch('https://opera8-action.hf.space/api/generate', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| prompt: vevoPayload, | |
| action_name: 'vevo-voice' | |
| }), | |
| }); | |
| if (!response.ok) { | |
| const errData = await response.json().catch(() => ({})); | |
| throw new Error(errData.message || `خطا در تخصیص صف گیتهاب: ${response.status}`); | |
| } | |
| const data = await response.json(); | |
| const finalRunId = data.run_id || currentRunId; | |
| return { | |
| job_id: finalRunId, | |
| total_chunks: 1, | |
| chunks: [], | |
| date: new Date().toLocaleString('fa-IR'), | |
| timestamp: Date.now(), | |
| progress: 0, | |
| status: 'started', | |
| type: metadata.type, | |
| modelName: metadata.modelName, | |
| modelImage: metadata.modelImage, | |
| backend: 'standard' | |
| }; | |
| }; | |
| export const uploadEnhancementJob = async (audioBlob: Blob): Promise<any> => { | |
| const currentRunId = "taq" + Math.random().toString(36).substring(2, 14); | |
| const ext = "wav"; // فرمت پیشفرض برای انتقال فایل باینری صوتی | |
| // ۱. آپلود محلی مستقیم فایل صوتی به سرور اصلی (پوشه موقت) | |
| const formData = new FormData(); | |
| formData.append('run_id', `${currentRunId}_input`); | |
| formData.append('ext', ext); | |
| formData.append('file', audioBlob); | |
| const uploadResponse = await fetch('https://opera8-action.hf.space/api/webhook/upload', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| if (!uploadResponse.ok) { | |
| throw new Error("خطا در انتقال فایل صوتی به سرور اصلی جهت تقویت کیفیت."); | |
| } | |
| // ۲. ایجاد ساختار دستور پیکربندی برای گیتهاب اکشنز | |
| const taqviatPayload = `VOICECONFIG_TAQVIAT_userRunId_${currentRunId}_solver_Midpoint_nfe_64_tau_0.5_denoising_true_ext_${ext}`; | |
| // ۳. ارسال دستور کار صوتی به سیستم مدیریت موازی | |
| const response = await fetch('https://opera8-action.hf.space/api/generate', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| prompt: taqviatPayload, | |
| action_name: 'taqviat-sada' | |
| }) | |
| }); | |
| if (!response.ok) { | |
| const errText = await response.text(); | |
| throw new Error(`پاسخ خطا از سرور مدل: ${response.status} - ${errText}`); | |
| } | |
| const result = await response.json(); | |
| const finalJobId = result.run_id || currentRunId; | |
| return { | |
| job_id: finalJobId, | |
| total_chunks: 1, | |
| chunks: [] | |
| }; | |
| }; | |
| export const checkEnhancementStatus = async (metadata: any): Promise<any> => { | |
| const jobId = metadata.job_id; | |
| // بررسی وضعیت از طریق پروکسی بکاند داخلی جهت تبدیل خودکار به بله | |
| const response = await fetch(`/api/status/${jobId}`); | |
| if (!response.ok) throw new Error("بررسی وضعیت تقویت صدا با خطا مواجه شد: " + response.status); | |
| const data = await response.json(); | |
| let status = 'processing'; | |
| if (data.status === 'ready') status = 'completed'; | |
| else if (data.status === 'failed' || data.status === 'error') status = 'failed'; | |
| return { | |
| status, | |
| filename: jobId + ".wav", // ارجاع به آدرس فایل خروجی تقویتشده نهایی | |
| downloadUrl: data.url // ذخیره لینک دائمی بله یا لوکال موقت | |
| }; | |
| }; | |
| export const getEnhancementDownloadUrl = (filename: string): string => { | |
| // این متد برای سازگاری در سوابق از downloadUrl مستقیم فیلد استفاده خواهد کرد. | |
| return `/static/audio/${filename}`; | |
| }; | |
| const generateBaseTTS = async (text: string, speakerId: string = "Algieba"): Promise<Blob> => { | |
| const response = await fetch(`${TTS_BASE_URL}/api/generate`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ text, speaker: speakerId, temperature: 0.9 }) | |
| }); | |
| if (!response.ok) throw new Error(`TTS Generation failed`); | |
| // دریافت مستقیم فایل صوتی به صورت باینری (Blob) | |
| return await response.blob(); | |
| }; | |
| export const fetchRefAudioAsFile = async (url: string, filename: string): Promise<File> => { | |
| const response = await fetch(url); | |
| const blob = await response.blob(); | |
| return new File([blob], filename, { type: 'audio/wav' }); | |
| }; | |
| export const checkJobStatus = async (job: JobData): Promise<StatusResponse> => { | |
| // هدایت بررسی فرآیندها به سمت اندپوینت پراکسی بکاند داخلی | |
| if (job.backend === 'legacy') return await legacyApi.checkLegacyStatus(job.job_id); | |
| if (job.backend === 'standard') { | |
| try { | |
| const response = await fetch(`/api/status/${job.job_id}`); | |
| if (!response.ok) throw new Error(`بررسی وضعیت با خطا مواجه شد: ${response.status}`); | |
| const data = await response.json(); | |
| let status: 'processing' | 'completed' | 'failed' | 'error' = 'processing'; | |
| if (data.status === 'ready') status = 'completed'; | |
| else if (data.status === 'failed') status = 'failed'; | |
| else if (data.status === 'not_found' || data.status === 'error') status = 'error'; | |
| return { | |
| status, | |
| progress: data.status === 'ready' ? 100 : (data.status === 'processing' ? 50 : 10), | |
| filename: data.url ? data.url.split('/').pop() : undefined, | |
| detail: data.message || (data.status === 'ready' ? 'تکمیل شد' : 'در حال پردازش در سرور ابری...'), | |
| downloadUrl: data.url ? data.url : undefined | |
| }; | |
| } catch (e) { | |
| return { status: 'processing', progress: 10, detail: 'درحال تلاش برای برقراری ارتباط با سرور...' }; | |
| } | |
| } | |
| // فرآیند فالبک تشخیص لایههای قدیمی | |
| const legacyNames = [ | |
| 'shadmehr', 'moein', 'billie', 'chavoshi', 'ghomayshi', 'yas', 'ferdosipour', 'khiabani', 'jalilvand', 'tahmasb', 'pourang', 'sponge', 'jenab', 'yousef', 'jomeong', 'musk', | |
| 'شادمهر عقیلی', 'معین', 'بیلی آیلیش', 'محسن چاوشی', 'سیاوش قمیشی', 'یاس', | |
| 'عادل فردوسیپور', 'جواد خیابانی', 'چنگیز جلیلوند', 'ناصر طهماسب', | |
| 'عمو پورنگ', 'باب اسفنجی', 'جناب خان', | |
| 'یوسف پیامبر', 'جومونگ', 'ایلان ماسک' | |
| ]; | |
| const isLegacy = (job.modelName && legacyNames.includes(job.modelName.toLowerCase())) || job.job_id.includes('-'); | |
| if (isLegacy) return await legacyApi.checkLegacyStatus(job.job_id); | |
| try { | |
| const response = await fetch(`/api/status/${job.job_id}`); | |
| if (!response.ok) throw new Error(`بررسی وضعیت با خطا مواجه شد: ${response.status}`); | |
| const data = await response.json(); | |
| let status: 'processing' | 'completed' | 'failed' | 'error' = 'processing'; | |
| if (data.status === 'ready') status = 'completed'; | |
| else if (data.status === 'failed') status = 'failed'; | |
| else if (data.status === 'not_found' || data.status === 'error') status = 'error'; | |
| return { | |
| status, | |
| progress: data.status === 'ready' ? 100 : (data.status === 'processing' ? 50 : 10), | |
| filename: data.url ? data.url.split('/').pop() : undefined, | |
| detail: data.message || (data.status === 'ready' ? 'تکمیل شد' : 'در حال پردازش در سرور ابری...'), | |
| downloadUrl: data.url ? data.url : undefined | |
| }; | |
| } catch (e) { | |
| return { status: 'processing', progress: 10, detail: 'درحال تلاش برای برقراری ارتباط با سرور...' }; | |
| } | |
| }; | |
| export const processUnifiedJob = async ( | |
| sourceFile: File | Blob | undefined, | |
| text: string | undefined, | |
| refFile: File | Blob, | |
| model: Model | null, | |
| mode: 'vc' | 'tts', | |
| pitch: number, | |
| onProgress: (msg: string, progress?: number) => void, | |
| jobType: 'custom' | 'model' | |
| ): Promise<JobData> => { | |
| let actualSource: File | Blob; | |
| if (mode === 'vc') { | |
| if (!sourceFile) throw new Error("فایل صوتی یافت نشد"); | |
| actualSource = sourceFile; | |
| } else { | |
| if (!text) throw new Error("متن یافت نشد"); | |
| let baseSpeaker = (model && model.targetGender === 'female') ? "Despina" : "Algieba"; | |
| onProgress(`تولید صدای پایه...`, 5); | |
| actualSource = await generateBaseTTS(text, baseSpeaker); | |
| onProgress("صدای پایه تولید شد.", 10); | |
| } | |
| const metadata = { type: jobType, modelName: model?.name, modelImage: typeof model?.image === 'string' ? model.image : undefined }; | |
| if (model && model.type === 'legacy_rvc' && model.legacyConfig) { | |
| onProgress("ارسال به سرور لگاسی...", 15); | |
| return await legacyApi.uploadLegacyJob(actualSource, model.legacyConfig.modelUrl, pitch, { name: model.name, image: metadata.modelImage }); | |
| } else { | |
| onProgress("ارسال به سرور تغییر صدا...", 20); | |
| return await uploadJob(actualSource, refFile, metadata); | |
| } | |
| }; |