Spaces:
Sleeping
Sleeping
| import os | |
| from fastapi import FastAPI | |
| from fastapi.responses import HTMLResponse | |
| import edge_tts | |
| import asyncio | |
| import tempfile | |
| from pathlib import Path | |
| app = FastAPI() | |
| def read_root(): | |
| return """ | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <title>Microsoft Neural TTS Studio</title> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width,initial-scale=1"> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| min-height: 100vh; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .container { | |
| background: rgba(255, 255, 255, 0.95); | |
| backdrop-filter: blur(10px); | |
| border-radius: 20px; | |
| padding: 40px; | |
| box-shadow: 0 20px 40px rgba(0,0,0,0.1); | |
| max-width: 800px; | |
| width: 90%; | |
| } | |
| h1 { | |
| color: #333; | |
| margin-bottom: 30px; | |
| text-align: center; | |
| font-size: 2.5rem; | |
| font-weight: 700; | |
| } | |
| .subtitle { | |
| text-align: center; | |
| color: #666; | |
| margin-bottom: 30px; | |
| font-size: 1.1rem; | |
| } | |
| .features { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |
| gap: 20px; | |
| margin: 30px 0; | |
| } | |
| .feature { | |
| background: linear-gradient(135deg, #667eea, #764ba2); | |
| color: white; | |
| padding: 20px; | |
| border-radius: 15px; | |
| text-align: center; | |
| } | |
| .feature h3 { margin-bottom: 10px; } | |
| .demo { | |
| background: #f8f9fa; | |
| border-radius: 10px; | |
| padding: 20px; | |
| margin: 20px 0; | |
| } | |
| textarea { | |
| width: 100%; | |
| height: 100px; | |
| border: 2px solid #e9ecef; | |
| border-radius: 8px; | |
| padding: 15px; | |
| font-size: 1rem; | |
| margin-bottom: 15px; | |
| } | |
| .voice-select { | |
| width: 100%; | |
| padding: 12px; | |
| border: 2px solid #e9ecef; | |
| border-radius: 8px; | |
| font-size: 1rem; | |
| margin-bottom: 15px; | |
| background: white; | |
| color: #333; | |
| max-height: 200px; | |
| } | |
| .voice-select optgroup { | |
| font-weight: 600; | |
| color: #667eea; | |
| font-size: 1.1rem; | |
| padding: 8px 0; | |
| border-bottom: 1px solid #e9ecef; | |
| } | |
| .voice-select option { | |
| padding: 8px 12px; | |
| font-size: 0.95rem; | |
| } | |
| .controls { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 15px; | |
| margin-bottom: 15px; | |
| } | |
| .slider-group { | |
| background: #f8f9fa; | |
| padding: 15px; | |
| border-radius: 8px; | |
| } | |
| .slider-group label { | |
| display: block; | |
| margin-bottom: 8px; | |
| font-weight: 500; | |
| color: #333; | |
| } | |
| .slider { | |
| width: 100%; | |
| -webkit-appearance: none; | |
| height: 6px; | |
| border-radius: 3px; | |
| background: #e9ecef; | |
| outline: none; | |
| } | |
| .slider::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| appearance: none; | |
| width: 20px; | |
| height: 20px; | |
| border-radius: 50%; | |
| background: #667eea; | |
| cursor: pointer; | |
| } | |
| .slider::-moz-range-thumb { | |
| width: 20px; | |
| height: 20px; | |
| border-radius: 50%; | |
| background: #667eea; | |
| cursor: pointer; | |
| } | |
| .speak-btn { | |
| background: linear-gradient(135deg, #28a745, #20c997); | |
| color: white; | |
| border: none; | |
| padding: 15px 30px; | |
| border-radius: 8px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| display: block; | |
| margin: 0 auto; | |
| font-size: 1.1rem; | |
| } | |
| .speak-btn:hover { transform: translateY(-2px); } | |
| .speak-btn:disabled { | |
| opacity: 0.6; | |
| cursor: not-allowed; | |
| transform: none; | |
| } | |
| .status { | |
| text-align: center; | |
| margin-top: 15px; | |
| font-weight: 500; | |
| color: #666; | |
| min-height: 24px; | |
| padding: 8px; | |
| border-radius: 6px; | |
| background: #f8f9fa; | |
| } | |
| .status.error { | |
| background: #f8d7da; | |
| color: #721c24; | |
| border: 1px solid #f5c6cb; | |
| } | |
| .status.success { | |
| background: #d4edda; | |
| color: #155724; | |
| border: 1px solid #c3e6cb; | |
| } | |
| .status.loading { | |
| background: #d1ecf1; | |
| color: #0c5460; | |
| border: 1px solid #bee5eb; | |
| } | |
| .audio-player { | |
| margin-top: 20px; | |
| text-align: center; | |
| } | |
| audio { | |
| width: 100%; | |
| border-radius: 8px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1>🎙️ Microsoft Neural TTS Studio</h1> | |
| <p class="subtitle">Professional Text-to-Speech with 200+ Neural Voices in 30+ Languages</p> | |
| <div class="features"> | |
| <div class="feature"> | |
| <h3>🌍 30+ Languages</h3> | |
| <p>Dutch, English, French, German, Spanish, Italian, Portuguese, Asian, European and more</p> | |
| </div> | |
| <div class="feature"> | |
| <h3>🎤 200+ Voices</h3> | |
| <p>Male, Female, Regional variants with Microsoft neural technology</p> | |
| </div> | |
| <div class="feature"> | |
| <h3>⚡ Fast & Free</h3> | |
| <p>No API keys required, instant synthesis, high-quality output</p> | |
| </div> | |
| </div> | |
| <div class="demo"> | |
| <h3>Try It Now</h3> | |
| <textarea id="text-input" placeholder="Type your text here...">Hello, this is a demonstration of Microsoft Neural TTS Studio! This is a professional text-to-speech application with 200+ high-quality neural voices in 30+ languages. Try different voices and languages to experience the power of Microsoft neural technology!</textarea> | |
| <select id="voice-select" class="voice-select"> | |
| <!-- Dutch Voices --> | |
| <optgroup label="🇳🇱 Dutch"> | |
| <option value="nl-NL-FennaNeural">Fenna (Female)</option> | |
| <option value="nl-NL-ColetteNeural">Colette (Female)</option> | |
| <option value="nl-NL-MaartenNeural">Maarten (Male)</option> | |
| <option value="nl-BE-DenaNeural">Dena (Female, Flemish)</option> | |
| <option value="nl-BE-ArnaudNeural">Arnaud (Male, Flemish)</option> | |
| </optgroup> | |
| <!-- English Voices --> | |
| <optgroup label="🇺🇸 English"> | |
| <option value="en-US-JennyNeural">Jenny (Female, US)</option> | |
| <option value="en-US-GuyNeural">Guy (Male, US)</option> | |
| <option value="en-US-AriaNeural">Aria (Female, US)</option> | |
| <option value="en-US-DavisNeural">Davis (Male, US)</option> | |
| <option value="en-US-SaraNeural">Sara (Female, US)</option> | |
| <option value="en-US-TonyNeural">Tony (Male, US)</option> | |
| <option value="en-US-NancyNeural">Nancy (Female, US)</option> | |
| <option value="en-US-AmberNeural">Amber (Female, US)</option> | |
| <option value="en-US-AnaNeural">Ana (Female, US)</option> | |
| <option value="en-US-BrandonNeural">Brandon (Male, US)</option> | |
| <option value="en-US-ChristopherNeural">Christopher (Male, US)</option> | |
| <option value="en-US-CoraNeural">Cora (Female, US)</option> | |
| <option value="en-US-ElizabethNeural">Elizabeth (Female, US)</option> | |
| <option value="en-US-EricNeural">Eric (Male, US)</option> | |
| <option value="en-US-MonicaNeural">Monica (Female, US)</option> | |
| <option value="en-GB-SoniaNeural">Sonia (Female, UK)</option> | |
| <option value="en-GB-RyanNeural">Ryan (Male, UK)</option> | |
| <option value="en-GB-LibbyNeural">Libby (Female, UK)</option> | |
| <option value="en-GB-ThomasNeural">Thomas (Male, UK)</option> | |
| <option value="en-GB-MiaNeural">Mia (Female, UK)</option> | |
| <option value="en-GB-EmmaNeural">Emma (Female, UK)</option> | |
| <option value="en-GB-OliverNeural">Oliver (Male, UK)</option> | |
| <option value="en-GB-AvaNeural">Ava (Female, UK)</option> | |
| <option value="en-GB-RebeccaNeural">Rebecca (Female, UK)</option> | |
| <option value="en-AU-NatashaNeural">Natasha (Female, Australian)</option> | |
| <option value="en-AU-WilliamNeural">William (Male, Australian)</option> | |
| <option value="en-AU-DaisyNeural">Daisy (Female, Australian)</option> | |
| <option value="en-CA-ClaraNeural">Clara (Female, Canadian)</option> | |
| <option value="en-CA-LiamNeural">Liam (Male, Canadian)</option> | |
| <option value="en-CA-HeatherNeural">Heather (Female, Canadian)</option> | |
| <option value="en-IN-NeerjaNeural">Neerja (Female, Indian)</option> | |
| <option value="en-IN-PrabhatNeural">Prabhat (Male, Indian)</option> | |
| <option value="en-IN-SwaraNeural">Swara (Female, Indian)</option> | |
| <option value="en-ZA-LeahNeural">Leah (Female, South African)</option> | |
| <option value="en-ZA-LukeNeural">Luke (Male, South African)</option> | |
| </optgroup> | |
| <!-- French Voices --> | |
| <optgroup label="🇫🇷 French"> | |
| <option value="fr-FR-DeniseNeural">Denise (Female)</option> | |
| <option value="fr-FR-HenriNeural">Henri (Male)</option> | |
| <option value="fr-FR-AlainNeural">Alain (Male)</option> | |
| <option value="fr-FR-ArielleNeural">Arielle (Female)</option> | |
| <option value="fr-FR-VivienneNeural">Vivienne (Female)</option> | |
| <option value="fr-FR-CelesteNeural">Celeste (Female)</option> | |
| <option value="fr-FR-ClaudineNeural">Claudine (Female)</option> | |
| <option value="fr-FR-JacquelineNeural">Jacqueline (Female)</option> | |
| <option value="fr-FR-JosephineNeural">Josephine (Female)</option> | |
| <option value="fr-FR-YvesNeural">Yves (Male)</option> | |
| <option value="fr-FR-PaulNeural">Paul (Male)</option> | |
| <option value="fr-FR-RemyNeural">Remy (Male)</option> | |
| <option value="fr-FR-MarcelNeural">Marcel (Male)</option> | |
| <option value="fr-FR-LouiseNeural">Louise (Female)</option> | |
| <option value="fr-FR-MargotNeural">Margot (Female)</option> | |
| <option value="fr-BE-CharlineNeural">Charline (Female, Belgian)</option> | |
| <option value="fr-CA-SylvieNeural">Sylvie (Female, Canadian)</option> | |
| <option value="fr-CA-AntoineNeural">Antoine (Male, Canadian)</option> | |
| <option value="fr-CA-JeanNeural">Jean (Male, Canadian)</option> | |
| <option value="fr-CH-AliciaNeural">Alicia (Female, Swiss)</option> | |
| <option value="fr-CH-FabienNeural">Fabien (Male, Swiss)</option> | |
| <option value="fr-CH-RaphaelNeural">Raphael (Male, Swiss)</option> | |
| </optgroup> | |
| <!-- German Voices --> | |
| <optgroup label="🇩🇪 German"> | |
| <option value="de-DE-KatjaNeural">Katja (Female)</option> | |
| <option value="de-DE-ConradNeural">Conrad (Male)</option> | |
| <option value="de-DE-AmalaNeural">Amala (Female)</option> | |
| <option value="de-DE-BerndNeural">Bernd (Male)</option> | |
| <option value="de-DE-ChristophNeural">Christoph (Male)</option> | |
| <option value="de-DE-ElkeNeural">Elke (Female)</option> | |
| <option value="de-DE-GiselaNeural">Gisela (Female)</option> | |
| <option value="de-DE-KillianNeural">Killian (Male)</option> | |
| <option value="de-DE-SeraphinaNeural">Seraphina (Female)</option> | |
| <option value="de-DE-LouisaNeural">Louisa (Female)</option> | |
| <option value="de-DE-MajaNeural">Maja (Female)</option> | |
| <option value="de-DE-FlorianNeural">Florian (Male)</option> | |
| <option value="de-DE-SabineNeural">Sabine (Female)</option> | |
| <option value="de-DE-GerhardNeural">Gerhard (Male)</option> | |
| <option value="de-DE-KlaraNeural">Klara (Female)</option> | |
| <option value="de-DE-TanjaNeural">Tanja (Female)</option> | |
| <option value="de-AT-IngridNeural">Ingrid (Female, Austrian)</option> | |
| <option value="de-AT-JonasNeural">Jonas (Male, Austrian)</option> | |
| <option value="de-CH-LeniNeural">Leni (Female, Swiss)</option> | |
| <option value="de-CH-JanNeural">Jan (Male, Swiss)</option> | |
| </optgroup> | |
| <!-- Spanish Voices --> | |
| <optgroup label="🇪🇸 Spanish"> | |
| <option value="es-ES-ElviraNeural">Elvira (Female)</option> | |
| <option value="es-ES-AlvaroNeural">Alvaro (Male)</option> | |
| <option value="es-ES-AbrilNeural">Abril (Female)</option> | |
| <option value="es-ES-ArnauNeural">Arnau (Male)</option> | |
| <option value="es-ES-DarioNeural">Dario (Male)</option> | |
| <option value="es-ES-EliasNeural">Elias (Male)</option> | |
| <option value="es-ES-EstrellaNeural">Estrella (Female)</option> | |
| <option value="es-ES-XimenaNeural">Ximena (Female)</option> | |
| <option value="es-ES-LauraNeural">Laura (Female)</option> | |
| <option value="es-ES-IreneNeural">Irene (Female)</option> | |
| <option value="es-ES-SofiaNeural">Sofia (Female)</option> | |
| <option value="es-ES-PabloNeural">Pablo (Male)</option> | |
| <option value="es-ES-ManuelNeural">Manuel (Male)</option> | |
| <option value="es-ES-PedroNeural">Pedro (Male)</option> | |
| <option value="es-MX-DaliaNeural">Dalia (Female, Mexican)</option> | |
| <option value="es-MX-JorgeNeural">Jorge (Male, Mexican)</option> | |
| <option value="es-MX-LuciaNeural">Lucia (Female, Mexican)</option> | |
| <option value="es-AR-AlejandraNeural">Alejandra (Female, Argentine)</option> | |
| <option value="es-AR-CastiNeural">Casti (Male, Argentine)</option> | |
| <option value="es-CO-SalomeNeural">Salome (Female, Colombian)</option> | |
| <option value="es-CO-GonzaloNeural">Gonzalo (Male, Colombian)</option> | |
| </optgroup> | |
| <!-- Italian Voices --> | |
| <optgroup label="🇮🇹 Italian"> | |
| <option value="it-IT-ElsaNeural">Elsa (Female)</option> | |
| <option value="it-IT-DiegoNeural">Diego (Male)</option> | |
| <option value="it-IT-FabiolaNeural">Fabiola (Female)</option> | |
| <option value="it-IT-GiuseppeNeural">Giuseppe (Male)</option> | |
| <option value="it-IT-IsabellaNeural">Isabella (Female)</option> | |
| <option value="it-IT-AlessandroNeural">Alessandro (Male)</option> | |
| <option value="it-IT-BenedettaNeural">Benedetta (Female)</option> | |
| <option value="it-IT-FrancescaNeural">Francesca (Female)</option> | |
| <option value="it-IT-MarcoNeural">Marco (Male)</option> | |
| <option value="it-IT-PaolaNeural">Paola (Female)</option> | |
| </optgroup> | |
| <!-- Portuguese Voices --> | |
| <optgroup label="🇧🇷 Portuguese"> | |
| <option value="pt-BR-FranciscaNeural">Francisca (Female, Brazilian)</option> | |
| <option value="pt-BR-AntonioNeural">Antonio (Male, Brazilian)</option> | |
| <option value="pt-BR-BrendaNeural">Brenda (Female, Brazilian)</option> | |
| <option value="pt-BR-ValerioNeural">Valerio (Male, Brazilian)</option> | |
| <option value="pt-BR-ThalitaNeural">Thalita (Female, Brazilian)</option> | |
| <option value="pt-BR-YaraNeural">Yara (Female, Brazilian)</option> | |
| <option value="pt-BR-CamilaNeural">Camila (Female, Brazilian)</option> | |
| <option value="pt-BR-JulianoNeural">Juliano (Male, Brazilian)</option> | |
| <option value="pt-BR-LeandroNeural">Leandro (Male, Brazilian)</option> | |
| <option value="pt-BR-NicolauNeural">Nicolau (Male, Brazilian)</option> | |
| <option value="pt-PT-RaquelNeural">Raquel (Female, Portuguese)</option> | |
| <option value="pt-PT-DuarteNeural">Duarte (Male, Portuguese)</option> | |
| <option value="pt-PT-MariaNeural">Maria (Female, Portuguese)</option> | |
| </optgroup> | |
| <!-- Asian Languages --> | |
| <optgroup label="🌏 Asian Languages"> | |
| <option value="zh-CN-XiaoxiaoNeural">Xiaoxiao (Female, Chinese)</option> | |
| <option value="zh-CN-YunyangNeural">Yunyang (Male, Chinese)</option> | |
| <option value="zh-CN-XiaoyiNeural">Xiaoyi (Female, Chinese)</option> | |
| <option value="zh-CN-YunjianNeural">Yunjian (Male, Chinese)</option> | |
| <option value="zh-CN-XiaochenNeural">Xiaochen (Female, Chinese)</option> | |
| <option value="zh-CN-XiaoxuanNeural">Xiaoxuan (Female, Chinese)</option> | |
| <option value="zh-CN-XiaomengNeural">Xiaomeng (Female, Chinese)</option> | |
| <option value="zh-CN-XiaoyanNeural">Xiaoyan (Female, Chinese)</option> | |
| <option value="zh-CN-XiaoyouNeural">Xiaoyou (Female, Chinese)</option> | |
| <option value="zh-CN-XiaoyuNeural">Xiaoyu (Female, Chinese)</option> | |
| <option value="zh-CN-HsiaochenNeural">Hsiaochen (Female, Chinese)</option> | |
| <option value="zh-CN-HsiaochengNeural">Hsiaocheng (Male, Chinese)</option> | |
| <option value="zh-CN-HsiaoyuNeural">Hsiaoyu (Female, Chinese)</option> | |
| <option value="ja-JP-NanamiNeural">Nanami (Female, Japanese)</option> | |
| <option value="ja-JP-KeitaNeural">Keita (Male, Japanese)</option> | |
| <option value="ja-JP-AoiNeural">Aoi (Female, Japanese)</option> | |
| <option value="ja-JP-NaomiNeural">Naomi (Female, Japanese)</option> | |
| <option value="ja-JP-DaichiNeural">Daichi (Male, Japanese)</option> | |
| <option value="ja-JP-MasaruNeural">Masaru (Male, Japanese)</option> | |
| <option value="ko-KR-SunHiNeural">SunHi (Female, Korean)</option> | |
| <option value="ko-KR-InJoonNeural">InJoon (Male, Korean)</option> | |
| <option value="ko-KR-BongJinNeural">BongJin (Male, Korean)</option> | |
| <option value="ko-KR-GookMinNeural">GookMin (Male, Korean)</option> | |
| <option value="ko-KR-JiMinNeural">JiMin (Female, Korean)</option> | |
| <option value="ko-KR-SeoHyeonNeural">SeoHyeon (Female, Korean)</option> | |
| <option value="ko-KR-HyunsuNeural">Hyunsu (Male, Korean)</option> | |
| <option value="ko-KR-SoonBokNeural">SoonBok (Male, Korean)</option> | |
| <option value="hi-IN-SwaraNeural">Swara (Female, Hindi)</option> | |
| <option value="hi-IN-MadhurNeural">Madhur (Male, Hindi)</option> | |
| <option value="hi-IN-AnjaliNeural">Anjali (Female, Hindi)</option> | |
| <option value="hi-IN-RajNeural">Raj (Male, Hindi)</option> | |
| <option value="he-IL-AvriNeural">Avri (Male, Hebrew)</option> | |
| <option value="he-IL-HilaNeural">Hila (Female, Hebrew)</option> | |
| <option value="ar-SA-ZariyahNeural">Zariyah (Female, Arabic)</option> | |
| <option value="ar-SA-HamedNeural">Hamed (Male, Arabic)</option> | |
| <option value="ar-SA-FatimaNeural">Fatima (Female, Arabic)</option> | |
| <option value="ar-SA-AliNeural">Ali (Male, Arabic)</option> | |
| <option value="ar-SA-OmarNeural">Omar (Male, Arabic)</option> | |
| </optgroup> | |
| <!-- European Languages --> | |
| <optgroup label="🌍 European Languages"> | |
| <option value="pl-PL-ZofiaNeural">Zofia (Female, Polish)</option> | |
| <option value="pl-PL-JacekNeural">Jacek (Male, Polish)</option> | |
| <option value="pl-PL-EwaNeural">Ewa (Female, Polish)</option> | |
| <option value="pl-PL-MarekNeural">Marek (Male, Polish)</option> | |
| <option value="pl-PL-JuliaNeural">Julia (Female, Polish)</option> | |
| <option value="pl-PL-JanNeural">Jan (Male, Polish)</option> | |
| <option value="pl-PL-MajaNeural">Maja (Female, Polish)</option> | |
| <option value="ro-RO-AlinaNeural">Alina (Female, Romanian)</option> | |
| <option value="ro-RO-EmilNeural">Emil (Male, Romanian)</option> | |
| <option value="hu-HU-NoemiNeural">Noemi (Female, Hungarian)</option> | |
| <option value="hu-HU-TamasNeural">Tamas (Male, Hungarian)</option> | |
| <option value="el-GR-AthinaNeural">Athina (Female, Greek)</option> | |
| <option value="el-GR-NestorasNeural">Nestoras (Male, Greek)</option> | |
| <option value="fi-FI-SelmaNeural">Selma (Female, Finnish)</option> | |
| <option value="fi-FI-HarriNeural">Harri (Male, Finnish)</option> | |
| <option value="fi-FI-SeljaNeural">Selja (Female, Finnish)</option> | |
| <option value="sv-SE-SofieNeural">Sofie (Female, Swedish)</option> | |
| <option value="sv-SE-MattiasNeural">Mattias (Male, Swedish)</option> | |
| <option value="sv-SE-AstridNeural">Astrid (Female, Swedish)</option> | |
| <option value="da-DK-ChristelNeural">Christel (Female, Danish)</option> | |
| <option value="da-DK-JeppeNeural">Jeppe (Male, Danish)</option> | |
| <option value="da-DK-MetteNeural">Mette (Female, Danish)</option> | |
| <option value="nb-NO-PernilleNeural">Pernille (Female, Norwegian)</option> | |
| <option value="nb-NO-FinnNeural">Finn (Male, Norwegian)</option> | |
| <option value="nb-NO-IselinNeural">Iselin (Female, Norwegian)</option> | |
| <option value="ru-RU-SvetlanaNeural">Svetlana (Female, Russian)</option> | |
| <option value="ru-RU-DmitryNeural">Dmitry (Male, Russian)</option> | |
| <option value="ru-RU-DariyaNeural">Dariya (Female, Russian)</option> | |
| <option value="ru-RU-PavelNeural">Pavel (Male, Russian)</option> | |
| <option value="tr-TR-EmelNeural">Emel (Female, Turkish)</option> | |
| <option value="tr-TR-AhmetNeural">Ahmet (Male, Turkish)</option> | |
| <option value="tr-TR-SultanNeural">Sultan (Male, Turkish)</option> | |
| </optgroup> | |
| </select> | |
| <div class="controls"> | |
| <div class="slider-group"> | |
| <label>Speed: <span id="speed-value">+0%</span></label> | |
| <input type="range" id="speed-slider" class="slider" min="-50" max="50" value="0"> | |
| </div> | |
| <div class="slider-group"> | |
| <label>Pitch: <span id="pitch-value">+0Hz</span></label> | |
| <input type="range" id="pitch-slider" class="slider" min="-50" max="50" value="0"> | |
| </div> | |
| </div> | |
| <button class="speak-btn" id="speak-btn" onclick="speak()">🔊 Speak Text</button> | |
| <div class="status" id="status">Ready to speak</div> | |
| <div class="audio-player" id="audio-player" style="display: none;"> | |
| <audio id="audio-element" controls></audio> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Update slider values | |
| document.getElementById('speed-slider').addEventListener('input', function() { | |
| const value = this.value; | |
| document.getElementById('speed-value').textContent = value >= 0 ? `+${value}%` : `${value}%`; | |
| }); | |
| document.getElementById('pitch-slider').addEventListener('input', function() { | |
| const value = this.value; | |
| document.getElementById('pitch-value').textContent = value >= 0 ? `+${value}Hz` : `${value}Hz`; | |
| }); | |
| async function speak() { | |
| const text = document.getElementById('text-input').value; | |
| const voice = document.getElementById('voice-select').value; | |
| const speed = document.getElementById('speed-slider').value; | |
| const pitch = document.getElementById('pitch-slider').value; | |
| if (!text.trim()) { | |
| updateStatus('Please enter some text', 'error'); | |
| return; | |
| } | |
| if (text.length > 5000) { | |
| updateStatus('Text too long! Maximum 5000 characters allowed.', 'error'); | |
| return; | |
| } | |
| const button = document.getElementById('speak-btn'); | |
| button.textContent = '⏳ Generating...'; | |
| button.disabled = true; | |
| updateStatus('Generating speech...', 'loading'); | |
| try { | |
| const response = await fetch('/synthesize', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'Accept': 'audio/mpeg' | |
| }, | |
| body: JSON.stringify({ | |
| text, | |
| voice, | |
| rate: speed >= 0 ? `+${speed}%` : `${speed}%`, | |
| pitch: pitch >= 0 ? `+${pitch}Hz` : `${pitch}Hz` | |
| }) | |
| }); | |
| if (response.ok) { | |
| const contentType = response.headers.get('content-type'); | |
| if (contentType && contentType.includes('audio/mpeg')) { | |
| const audioBlob = await response.blob(); | |
| const audioUrl = URL.createObjectURL(audioBlob); | |
| const audioElement = document.getElementById('audio-element'); | |
| audioElement.src = audioUrl; | |
| document.getElementById('audio-player').style.display = 'block'; | |
| audioElement.play(); | |
| updateStatus('Playing audio...', 'success'); | |
| } else { | |
| const errorData = await response.json(); | |
| updateStatus(`Error: ${errorData.error || 'Unknown error'}`, 'error'); | |
| } | |
| } else { | |
| const errorData = await response.json(); | |
| updateStatus(`Speech synthesis failed: ${errorData.error || 'Unknown error'}`, 'error'); | |
| } | |
| } catch (error) { | |
| updateStatus(`Network error: ${error.message}`, 'error'); | |
| console.error('Synthesis error:', error); | |
| } finally { | |
| button.textContent = '🔊 Speak Text'; | |
| button.disabled = false; | |
| } | |
| } | |
| function updateStatus(message, type) { | |
| const statusElement = document.getElementById('status'); | |
| statusElement.textContent = message; | |
| statusElement.className = 'status'; | |
| if (type) { | |
| statusElement.classList.add(type); | |
| } | |
| } | |
| // Auto-play when audio ends | |
| document.getElementById('audio-element').addEventListener('ended', function() { | |
| updateStatus('Ready to speak', 'normal'); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| async def synthesize(request): | |
| try: | |
| data = await request.json() | |
| text = data.get("text", "") | |
| voice = data.get("voice", "en-US-JennyNeural") | |
| rate = data.get("rate", "+0%") | |
| pitch = data.get("pitch", "+0Hz") | |
| if not text: | |
| return {"error": "Text is required"} | |
| # Validate text length (Edge TTS has limits) | |
| if len(text) > 5000: | |
| return {"error": "Text too long. Maximum 5000 characters allowed."} | |
| # Validate voice format | |
| if not voice or not isinstance(voice, str): | |
| return {"error": "Invalid voice format"} | |
| # Create communicate object with error handling | |
| try: | |
| communicate = edge_tts.Communicate(text, voice, rate=rate, pitch=pitch) | |
| except Exception as e: | |
| return {"error": f"Voice initialization failed: {str(e)}"} | |
| # Generate audio with timeout and error handling | |
| try: | |
| audio_data = await communicate.get_audio_data() | |
| if not audio_data: | |
| return {"error": "No audio data generated"} | |
| # Return audio as response | |
| from fastapi.responses import Response | |
| return Response( | |
| content=audio_data, | |
| media_type="audio/mpeg", | |
| headers={ | |
| "Content-Disposition": "inline; filename=speech.mp3", | |
| "Cache-Control": "no-cache" | |
| } | |
| ) | |
| except Exception as e: | |
| return {"error": f"Audio generation failed: {str(e)}"} | |
| except Exception as e: | |
| return {"error": f"Request processing failed: {str(e)}"} | |
| async def get_voices(): | |
| """Get available voices for debugging""" | |
| try: | |
| voices = await edge_tts.list_voices() | |
| return {"voices": voices[:50]} # Return first 50 voices for testing | |
| except Exception as e: | |
| return {"error": f"Failed to get voices: {str(e)}"} | |
| async def test_endpoint(): | |
| """Test endpoint to verify server is working""" | |
| return {"status": "ok", "message": "Microsoft Neural TTS Studio is working!"} | |