import os from TTS.utils.synthesizer import Synthesizer import gradio as gr from huggingface_hub import hf_hub_download from huggingface_hub import login import time # Uncomment for private models if needed # login(token=os.environ.get("HF_TOKEN")) # Custom CSS for better styling - بهینه‌سازی شده با طراحی حرفه‌ای‌تر دسکتاپ custom_css = """ /* فایل CSS مدرن و حرفه‌ای برای سیستم تبدیل متن فارسی به گفتار - نسخه دسکتاپ 3.0 */ @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700;800&display=swap'); :root { /* پالت رنگی جدید - ترکیب آبی و بنفش حرفه‌ای */ --primary-color: #4f46e5; --primary-light: #818cf8; --primary-dark: #3730a3; --primary-gradient: linear-gradient(135deg, #4f46e5, #6366f1); --primary-glass: rgba(79, 70, 229, 0.1); /* رنگ‌های ثانویه - طیف سبز آبی برای تضاد */ --secondary-color: #0ea5e9; --secondary-light: #7dd3fc; --secondary-dark: #0369a1; --secondary-gradient: linear-gradient(135deg, #0ea5e9, #38bdf8); /* رنگ‌های خنثی - طیف خاکستری سرد برای رابط دسکتاپ */ --background-color: #f8fafc; --surface-color: #ffffff; --text-color: #334155; --text-light: #64748b; --text-dark: #1e293b; --heading-color: #0f172a; --border-color: #e2e8f0; --divider-color: #f1f5f9; /* رنگ‌های وضعیت بهبود یافته */ --success-color: #10b981; --success-light: #d1fae5; --warning-color: #f59e0b; --warning-light: #fef3c7; --error-color: #ef4444; --error-light: #fee2e2; --info-color: #3b82f6; --info-light: #dbeafe; /* سایه‌های بهبود یافته برای حالت دسکتاپ */ --shadow-xs: 0 1px 2px rgba(15, 23, 42, 0.05); --shadow-sm: 0 2px 4px rgba(15, 23, 42, 0.07); --shadow-md: 0 4px 8px rgba(15, 23, 42, 0.08); --shadow-lg: 0 12px 24px rgba(15, 23, 42, 0.09); --shadow-xl: 0 20px 40px rgba(15, 23, 42, 0.1); --shadow-focus: 0 0 0 3px rgba(79, 70, 229, 0.3); /* انیمیشن‌ها */ --transition-fast: all 0.2s ease; --transition: all 0.3s ease; --transition-slow: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); /* اندازه‌های گوشه‌ها */ --border-radius-sm: 8px; --border-radius: 12px; --border-radius-lg: 16px; --border-radius-xl: 24px; --border-radius-full: 9999px; /* اندازه فاصله‌ها */ --spacing-xs: 0.25rem; --spacing-sm: 0.5rem; --spacing-md: 1rem; --spacing-lg: 1.5rem; --spacing-xl: 2rem; --spacing-2xl: 3rem; } /* تنظیمات پایه */ body { font-family: 'Vazirmatn', system-ui, -apple-system, sans-serif; background-color: var(--background-color); margin: 0; padding: 0; color: var(--text-color); line-height: 1.6; } /* کانتینر اصلی - طراحی دسکتاپ */ .gradio-container { background: linear-gradient(135deg, #f0f9ff 0%, #e6f2ff 100%); font-family: 'Vazirmatn', system-ui, sans-serif !important; color: var(--text-color); max-width: 1400px !important; margin: 0 auto !important; min-height: 100vh; display: flex; flex-direction: column; } /* سربرگ اصلی - سبک دسکتاپ */ .main-header { color: var(--heading-color); text-align: center; margin: 2rem 0; font-size: 2.8rem; font-weight: 800; line-height: 1.2; letter-spacing: -0.025em; text-shadow: 0px 2px 4px rgba(0,0,0,0.1); position: relative; font-family: 'Vazirmatn', system-ui, sans-serif !important; } .main-header:after { content: ""; display: block; width: 140px; height: 5px; background: var(--primary-gradient); margin: 1rem auto; border-radius: var(--border-radius-full); } /* لوگو و آیکون‌ها */ .logo-area { display: flex; justify-content: center; align-items: center; gap: 1rem; margin-bottom: 1.5rem; } .app-logo { font-size: 2rem; background: var(--primary-gradient); -webkit-background-clip: text; color: transparent; font-weight: 700; } /* کانتینر اصلی محتوا - طراحی دسکتاپ */ .app-container { display: flex; flex-direction: column; max-width: 1200px; margin: 0 auto 2.5rem; padding: 0; flex: 1; } .desktop-layout { display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; align-items: start; } /* پنل اصلی */ .main-panel { background-color: rgba(255, 255, 255, 0.95); border-radius: var(--border-radius-lg); box-shadow: var(--shadow-xl); backdrop-filter: blur(15px); border: 1px solid rgba(255, 255, 255, 0.7); padding: 2.5rem; position: relative; overflow: hidden; } /* افکت تزئینی پس‌زمینه */ .main-panel:before { content: ""; position: absolute; top: -50%; left: -50%; width: 200%; height: 200%; background: radial-gradient(circle, var(--primary-glass) 0%, transparent 70%); opacity: 0.5; z-index: 0; pointer-events: none; } /* نوار عنوان پنل‌ها */ .panel-title { background: var(--primary-gradient); color: white; padding: 1rem 1.5rem; border-radius: var(--border-radius) var(--border-radius) 0 0; font-weight: 600; font-size: 1.1rem; margin: -2.5rem -2.5rem 1.5rem -2.5rem; display: flex; align-items: center; justify-content: space-between; } .panel-title-icon { font-size: 1.2rem; margin-left: 0.5rem; } /* فوتر */ .footer { text-align: center; margin-top: 3rem; color: var(--text-light); font-size: 0.95rem; line-height: 1.6; padding: 0.75rem; position: relative; z-index: 1; background-color: rgba(255, 255, 255, 0.5); backdrop-filter: blur(10px); border-top: 1px solid var(--border-color); } /* تنظیمات متن فارسی */ textarea, .label, #text-input { text-align: right !important; direction: rtl !important; font-family: 'Vazirmatn', system-ui, sans-serif !important; } .label { font-size: 1.1rem !important; font-weight: 600 !important; color: var(--heading-color) !important; margin-bottom: 0.75rem !important; display: block; font-family: 'Vazirmatn', system-ui, sans-serif !important; } /* استایل دکمه‌ها */ button.primary { background: var(--primary-gradient) !important; border: none !important; border-radius: var(--border-radius) !important; color: white !important; font-weight: 600 !important; font-size: 1.05rem !important; padding: 0.8rem 1.75rem !important; transition: var(--transition) !important; box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3) !important; font-family: 'Vazirmatn', system-ui, sans-serif !important; position: relative; overflow: hidden; } button.primary:hover { transform: translateY(-2px) !important; box-shadow: 0 8px 16px rgba(79, 70, 229, 0.4) !important; background: linear-gradient(45deg, #3730a3, #4f46e5) !important; } button.primary:active { transform: translateY(0) !important; box-shadow: 0 2px 8px rgba(79, 70, 229, 0.4) !important; } /* افکت موج دکمه */ button.primary:after { content: ""; position: absolute; top: 50%; left: 50%; width: 5px; height: 5px; background: rgba(255, 255, 255, 0.7); opacity: 0; border-radius: 100%; transform: scale(1, 1) translate(-50%); transform-origin: 50% 50%; } button.primary:focus:not(:active)::after { animation: ripple 1s ease-out; } @keyframes ripple { 0% { transform: scale(0, 0); opacity: 0.5; } 20% { transform: scale(25, 25); opacity: 0.3; } 100% { opacity: 0; transform: scale(40, 40); } } /* پنل‌های ورودی و خروجی */ .input-panel, .output-panel { background-color: var(--surface-color); border-radius: var(--border-radius); padding: 1.5rem; margin-bottom: 1.5rem; border: 1px solid var(--border-color); transition: var(--transition); box-shadow: var(--shadow-sm); position: relative; z-index: 2; } .input-panel:hover, .output-panel:hover { box-shadow: var(--shadow-md); border-color: var(--primary-light); } /* کادر متن */ .input-text textarea { border: 2px solid var(--border-color) !important; border-radius: var(--border-radius) !important; padding: 1.25rem !important; transition: var(--transition) !important; font-size: 1.05rem !important; background-color: rgba(255, 255, 255, 0.8) !important; box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.03) !important; font-family: 'Vazirmatn', system-ui, sans-serif !important; color: var(--text-color) !important; line-height: 1.8 !important; } .input-text textarea:focus { border-color: var(--primary-color) !important; box-shadow: var(--shadow-focus), inset 0 2px 4px rgba(0, 0, 0, 0.03) !important; background-color: rgba(255, 255, 255, 1) !important; outline: none !important; } /* رنگ متن در کانتینر */ .gradio-container textarea { color: var(--text-color) !important; } /* اسلایدر سرعت */ .speed-slider-container { background: linear-gradient(to right, rgba(255,255,255,0.9), rgba(240,246,255,0.9)); border-radius: var(--border-radius); padding: 1.25rem; margin-top: 1.5rem; border: 1px solid var(--border-color); box-shadow: var(--shadow-sm); } .speed-slider input[type="range"] { height: 8px !important; border-radius: var(--border-radius-full) !important; background: linear-gradient(90deg, var(--primary-light), var(--secondary-light)) !important; } .speed-slider input[type="range"]::-webkit-slider-thumb { background: var(--primary-gradient) !important; border: 3px solid white !important; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15) !important; height: 20px !important; width: 20px !important; border-radius: 50% !important; } .speed-slider .label { font-size: 1rem !important; color: var(--text-color) !important; font-weight: 500 !important; } /* پنل مثال‌ها */ .examples-panel { background-color: rgba(248, 250, 252, 0.8); border-radius: var(--border-radius); padding: 1.5rem; border: 1px solid var(--border-color); margin-top: 2rem; position: relative; z-index: 2; } /* پنل وضعیت با کلاس‌های مختلف */ .status-panel { background: linear-gradient(135deg, #f8fafc, #f1f5f9); border-radius: var(--border-radius); padding: 1rem; margin: 0 0 1.5rem 0; text-align: center; font-weight: 500; border-right: 4px solid var(--primary-color); transition: var(--transition); font-family: 'Vazirmatn', system-ui, sans-serif !important; position: relative; overflow: hidden; box-shadow: var(--shadow-sm); } .status-success { color: var(--success-color) !important; background: linear-gradient(135deg, #f0fdf4, #dcfce7) !important; border-right-color: var(--success-color) !important; } .status-error { color: var(--error-color) !important; background: linear-gradient(135deg, #fef2f2, #fee2e2) !important; border-right-color: var(--error-color) !important; } .status-processing { color: var(--info-color) !important; background: linear-gradient(135deg, #eff6ff, #dbeafe) !important; border-right-color: var(--info-color) !important; background-size: 200% 200% !important; animation: wave 2s ease infinite !important; } /* استایل پخش‌کننده صوتی */ audio { border-radius: var(--border-radius) !important; background: linear-gradient(to right, #dbeafe, #e0e7ff) !important; box-shadow: var(--shadow-sm) !important; width: 100% !important; transition: var(--transition) !important; } audio:hover { box-shadow: var(--shadow-md) !important; } /* استایل دکمه‌های مثال */ .examples-panel button { border: 1px solid #e2e8f0 !important; background: rgba(255, 255, 255, 0.8) !important; border-radius: var(--border-radius-sm) !important; transition: var(--transition-fast) !important; font-family: 'Vazirmatn', system-ui, sans-serif !important; color: var(--text-color) !important; } .examples-panel button:hover { background: white !important; border-color: var(--primary-light) !important; transform: translateY(-2px) !important; box-shadow: var(--shadow-sm) !important; color: var(--primary-dark) !important; } /* طرح دکمه بزرگ */ .big-button { font-size: 1.1rem !important; padding: 1rem 2rem !important; width: 100% !important; margin-top: 1rem !important; text-align: center !important; } /* نوار پیشرفت */ .progress-bar { height: 8px; background-color: var(--border-color); border-radius: var(--border-radius-full); margin: 0.5rem 0; overflow: hidden; } .progress-bar-fill { height: 100%; background: var(--primary-gradient); border-radius: var(--border-radius-full); transition: width 0.3s ease; width: 0%; } /* انیمیشن‌های جلوه‌ای */ @keyframes fadeIn { from { opacity: 0; transform: translateY(15px); } to { opacity: 1; transform: translateY(0); } } .desktop-layout { animation: fadeIn 0.7s ease-out; } /* افکت موج برای پردازش صوتی */ @keyframes wave { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } } /* حالت شب و روز */ .theme-switch { position: absolute; top: 1rem; right: 1rem; background: var(--secondary-gradient); color: white; width: 2.5rem; height: 2.5rem; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; box-shadow: var(--shadow-md); z-index: 10; transition: var(--transition); } .theme-switch:hover { transform: scale(1.1); } /* پشتیبانی RTL برای تمام عناصر UI */ .gradio-container [dir="rtl"], .gradio-container [style*="direction: rtl"] { font-family: 'Vazirmatn', system-ui, sans-serif !important; } /* اصلاح المان‌های مارک‌داون */ .prose p, .prose h1, .prose h2, .prose h3, .prose h4, .prose h5, .prose h6 { direction: rtl !important; text-align: right !important; font-family: 'Vazirmatn', system-ui, sans-serif !important; color: var(--text-color) !important; } /* استایل لینک‌ها */ a { color: var(--primary-color) !important; text-decoration: none !important; transition: var(--transition-fast) !important; font-weight: 500 !important; } a:hover { color: var(--primary-dark) !important; text-decoration: underline !important; } /* کارت‌های اطلاعات */ .info-card { background: linear-gradient(135deg, rgba(255,255,255,0.9), rgba(240,246,255,0.9)); border-radius: var(--border-radius); padding: 1.25rem; margin-bottom: 1.5rem; border: 1px solid var(--border-color); transition: var(--transition); position: relative; z-index: 2; } .info-card:hover { box-shadow: var(--shadow-md); border-color: var(--primary-light); } /* واکنش‌گرایی برای نمایشگرهای مختلف */ @media (max-width: 1024px) { .desktop-layout { grid-template-columns: 1fr; } .main-panel { padding: 2rem; } .panel-title { margin: -2rem -2rem 1.5rem -2rem; } } @media (max-width: 768px) { .main-panel { padding: 1.5rem; } .panel-title { margin: -1.5rem -1.5rem 1.25rem -1.5rem; padding: 0.75rem 1.25rem; } .main-header { font-size: 2rem; margin: 1.5rem 0; } .label { font-size: 1rem !important; } .input-panel, .output-panel { padding: 1.25rem; } } /* بهینه‌سازی برای نمایشگرهای کوچک */ @media (max-width: 480px) { .main-panel { padding: 1.25rem; border-radius: var(--border-radius); } .panel-title { margin: -1.25rem -1.25rem 1rem -1.25rem; padding: 0.75rem 1rem; } .main-header { font-size: 1.5rem; } button.primary { width: 100%; padding: 0.7rem 1rem !important; } } /* اصلاحات سبک برای گردیو 4 */ .gr-padded { padding: var(--spacing-lg) !important; } .gr-gap { gap: var(--spacing-md) !important; } .gradio-button { border-radius: var(--border-radius) !important; } /* اصلاح فاصله‌ها */ .gr-panel > .gr-block { margin-bottom: var(--spacing-md) !important; } .gr-form > .gr-block { margin-bottom: var(--spacing-lg) !important; } /* بهبود المان‌های گردیو 4 */ .gr-box, .gr-block, .gr-input, .gr-textbox { border-radius: var(--border-radius) !important; } .gr-form { border: none !important; background: transparent !important; } .gr-accordion { border-radius: var(--border-radius) !important; overflow: hidden !important; } .gr-accordion-title { font-weight: 600 !important; color: var(--heading-color) !important; } .gr-panel { border-radius: var(--border-radius) !important; } /* استایل‌های خاص برای گردیو 4 */ .gr-input, .gr-textbox { transition: var(--transition) !important; } .gr-input:focus, .gr-textbox:focus { border-color: var(--primary-color) !important; box-shadow: var(--shadow-focus) !important; } .gr-padded-container { padding: var(--spacing-lg) !important; } .gr-compact { margin: 0 !important; } .gr-hover-container:hover { transform: translateY(-2px) !important; box-shadow: var(--shadow-md) !important; } .gr-interface { margin: 0 !important; } /* برخی از اصلاحات مهم برای Gradio 4 */ .gr-form { border: none !important; background: transparent !important; } .gr-form-input { border-radius: var(--border-radius) !important; transition: var(--transition) !important; } .gr-form-input:focus { border-color: var(--primary-color) !important; box-shadow: var(--shadow-focus) !important; } .gr-block { margin-bottom: var(--spacing-md) !important; } .gr-input-label { color: var(--heading-color) !important; font-weight: 600 !important; margin-bottom: var(--spacing-xs) !important; } .gr-interface { margin: 0 !important; } /* اصلاحات اضافی */ svg.gr-arrow { fill: var(--primary-color) !important; } .dark-mode { --background-color: #0f172a; --surface-color: #1e293b; --text-color: #e2e8f0; --text-light: #94a3b8; --text-dark: #f8fafc; --heading-color: #f1f5f9; --border-color: #334155; --divider-color: #1e293b; } """ def load_synthesizer(): # Update status message status = "در حال بارگذاری مدل... لطفاً منتظر بمانید" try: # Download model files from Hugging Face Hub model_path = hf_hub_download( repo_id="QomSSLab/vits-fa-voice", filename="best_model.pth", cache_dir="models" ) config_path = hf_hub_download( repo_id="QomSSLab/vits-fa-voice", filename="config.json", cache_dir="models" ) # Create synthesizer synthesizer = Synthesizer( tts_checkpoint=model_path, tts_config_path=config_path, use_cuda=False # Usually no GPU in free Spaces ) status = "✅ مدل با موفقیت بارگذاری شد! اکنون می‌توانید از سیستم استفاده کنید." return synthesizer, status except Exception as e: error_msg = f"خطا در بارگذاری مدل: {str(e)}" status = f"❌ {error_msg}" raise RuntimeError(error_msg) def tts(text, speed): if not text.strip(): return None, "لطفاً متنی وارد کنید.", "❌ لطفاً متنی وارد کنید." try: status = "در حال تبدیل متن به گفتار..." # Generate speech wav = synthesizer.tts(text, speed=speed) output_path = "output.wav" synthesizer.save_wav(wav, output_path) status = "✅ صدا با موفقیت تولید شد!" return output_path, "تبدیل با موفقیت انجام شد.", status except Exception as e: error_msg = f"خطا در تولید صدا: {str(e)}" status = f"❌ {error_msg}" return None, error_msg, status # JavaScript function for updating status classes def update_status_classes(status_text): # This will be used with the gradio.js code return status_text # Create the interface with improved layout and professional design with gr.Blocks(css=custom_css) as demo: with gr.Column(elem_classes="container"): gr.Markdown("# سامانه تبدیل متن فارسی به گفتار", elem_classes="main-header") # Status area with gr.Column(elem_classes="status-panel") as status_container: status_output = gr.Markdown("در حال آماده‌سازی سیستم...", elem_id="status") # Input panel with gr.Column(elem_classes="input-panel"): gr.Markdown("### متن ورودی", elem_classes="label") text_input = gr.Textbox( placeholder="متن فارسی خود را اینجا وارد کنید...", lines=5, label="", elem_id="text-input", elem_classes="input-text" ) with gr.Row(): speed_slider = gr.Slider( minimum=0.5, maximum=2.0, value=1.0, step=0.1, label="سرعت گفتار", elem_classes="speed-slider" ) submit_btn = gr.Button("تبدیل به گفتار", variant="primary", elem_classes="primary") # Output panel with gr.Column(elem_classes="output-panel"): gr.Markdown("### خروجی صوتی", elem_classes="label") output_audio = gr.Audio(label="") result_text = gr.Markdown("") # Examples panel with gr.Column(elem_classes="examples-panel"): gr.Markdown("### نمونه‌های متنی", elem_classes="label") examples = gr.Examples( examples=[ ["سَلامْ دُنیا این یک آزمایش برای سیستم تبدیل متن به گفتارِ فارِسی است."], ["اِمروز هوا بسیار خوب است و من احساسِ شادی می‌کنم."], ["فَناوریِ هوشَ مصنوعیْ به سرعت در حالِ پیشرفت است و به زودی در تمام جنبه‌های زْندِگِیَ ما حضور خواهد داشت."] ], inputs=text_input, label="نمونه‌های متنی را امتحان کنید" ) gr.Markdown( "**راهنما**: متن فارسی خود را در کادر بالا وارد کنید و دکمه تبدیل را فشار دهید. " "می‌توانید سرعت گفتار را با استفاده از نوار لغزنده تنظیم کنید.", elem_classes="footer" ) gr.Markdown( "توسعه داده شده با استفاده از مدل VITS فارسی | [QomSSLab/vits-fa-voice](https://huggingface.co/QomSSLab/vits-fa-voice)", elem_classes="footer" ) # Add JavaScript for dynamic class changes - Gradio 4.0+ way demo.load(js=""" function updateStatusClass(status_text) { const statusPanel = document.querySelector('.status-panel'); if (!statusPanel) return; // Remove all status classes statusPanel.classList.remove('status-success', 'status-error', 'status-processing'); // Add appropriate class based on status content if (status_text.includes('✅')) { statusPanel.classList.add('status-success'); } else if (status_text.includes('❌')) { statusPanel.classList.add('status-error'); } else if (status_text.includes('در حال')) { statusPanel.classList.add('status-processing'); } } // Monitor changes to the status element const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.type === 'childList') { const statusEl = document.querySelector('#status'); if (statusEl) { updateStatusClass(statusEl.textContent); } } }); }); // Start observing when the DOM is ready document.addEventListener('DOMContentLoaded', () => { const statusEl = document.querySelector('#status'); if (statusEl) { observer.observe(statusEl, { childList: true, subtree: true }); // Initial update updateStatusClass(statusEl.textContent); } }); """) # Initialize the synthesizer on app startup synthesizer = None @demo.load def init_synthesizer(): global synthesizer try: synthesizer, status = load_synthesizer() return status_output.update(status) except Exception as e: error_status = f"❌ خطا در بارگذاری مدل: {str(e)}" return status_output.update(error_status) # Define the tts_wrapper to handle status updates def tts_wrapper(text, speed): audio, result, status = tts(text, speed) return audio, result, status # Connect the function to the button submit_btn.click( fn=tts_wrapper, inputs=[text_input, speed_slider], outputs=[output_audio, result_text, status_output] ) # Launch the interface demo.launch()