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 => { 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 => { 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 => { 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 => { 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 => { 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 => { 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 => { // هدایت بررسی فرآیندها به سمت اندپوینت پراکسی بک‌اند داخلی 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 => { 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); } };