Spaces:
Build error
Build error
| # app.py | |
| from fastapi import FastAPI, Request, File, UploadFile, Form | |
| from fastapi.responses import HTMLResponse, FileResponse | |
| from pydantic import BaseModel | |
| import tempfile | |
| import os | |
| import sys | |
| import subprocess | |
| import traceback | |
| import torchaudio | |
| from modelscope import snapshot_download | |
| import threading | |
| # ์ ์ญ ๋ฝ ๊ฐ์ฒด ์์ฑ | |
| tts_lock = threading.Lock() | |
| # ---------------- CosyVoice ๊ฒฝ๋ก ์ค์ ---------------- | |
| sys.path.append('/app/model') | |
| sys.path.append('/app/model/third_party/Matcha-TTS') | |
| from cosyvoice.cli.cosyvoice import CosyVoice2 | |
| from cosyvoice.utils.file_utils import load_wav | |
| # ---------------- ์ ์ญ ๋ณ์ ---------------- | |
| cosyvoice_model = None | |
| # ---------------- ๋ชจ๋ธ ์ด๊ธฐํ ํจ์ ---------------- | |
| def initialize_cosyvoice(): | |
| """CosyVoice2 ๋ชจ๋ธ์ ์ด๊ธฐํํฉ๋๋ค.""" | |
| global cosyvoice_model | |
| try: | |
| print("=== CosyVoice2 ๋ชจ๋ธ ์ด๊ธฐํ ์์ ===") | |
| # ์์ ๋๋ ํ ๋ฆฌ๋ฅผ cosyvoice ๋ชจ๋ ์์น๋ก ๋ณ๊ฒฝ | |
| original_cwd = os.getcwd() | |
| cosyvoice_dir = '/app/model/cosyvoice' | |
| print(f"์์ ๋๋ ํ ๋ฆฌ ๋ณ๊ฒฝ: {original_cwd} -> {cosyvoice_dir}") | |
| os.chdir(cosyvoice_dir) | |
| # ๋ชจ๋ธ ๊ฒฝ๋ก ํ์ธ | |
| model_path = '/app/pretrained_models/CosyVoice2-0.5B' | |
| ttsfrd_path = '/app/pretrained_models/CosyVoice-ttsfrd' | |
| resource_path = '/app/pretrained_models/CosyVoice-ttsfrd/resource' | |
| print(f"๋ชจ๋ธ ๊ฒฝ๋ก ํ์ธ: {model_path}") | |
| print(f"๋ชจ๋ธ ๊ฒฝ๋ก ์กด์ฌ: {os.path.exists(model_path)}") | |
| print(f"ttsfrd ๊ฒฝ๋ก ํ์ธ: {ttsfrd_path}") | |
| print(f"ttsfrd ๊ฒฝ๋ก ์กด์ฌ: {os.path.exists(ttsfrd_path)}") | |
| print(f"๋ฆฌ์์ค ๊ฒฝ๋ก ํ์ธ: {resource_path}") | |
| print(f"๋ฆฌ์์ค ๊ฒฝ๋ก ์กด์ฌ: {os.path.exists(resource_path)}") | |
| if os.path.exists(ttsfrd_path): | |
| print("ttsfrd ๋๋ ํ ๋ฆฌ ๋ด์ฉ:") | |
| for item in os.listdir(ttsfrd_path): | |
| item_path = os.path.join(ttsfrd_path, item) | |
| print(f" {item} ({'dir' if os.path.isdir(item_path) else 'file'})") | |
| if os.path.exists(resource_path): | |
| print("resource ๋๋ ํ ๋ฆฌ ๋ด์ฉ:") | |
| for item in os.listdir(resource_path): | |
| print(f" {item}") | |
| if not os.path.exists(model_path): | |
| print(f"โ ๋ชจ๋ธ ๊ฒฝ๋ก๊ฐ ์กด์ฌํ์ง ์์ต๋๋ค: {model_path}") | |
| return False | |
| if not os.path.exists(resource_path): | |
| print(f"โ ๋ฆฌ์์ค ๊ฒฝ๋ก๊ฐ ์กด์ฌํ์ง ์์ต๋๋ค: {resource_path}") | |
| return False | |
| # ROOT_DIR ๊ธฐ์ค ์๋ ๊ฒฝ๋ก ํ์ธ | |
| expected_resource_path = os.path.join(os.getcwd(), '../../pretrained_models/CosyVoice-ttsfrd/resource') | |
| normalized_path = os.path.normpath(expected_resource_path) | |
| print(f"CosyVoice๊ฐ ์ฐพ๋ ๋ฆฌ์์ค ๊ฒฝ๋ก: {normalized_path}") | |
| print(f"ํด๋น ๊ฒฝ๋ก ์กด์ฌ ์ฌ๋ถ: {os.path.exists(normalized_path)}") | |
| # ๋ชจ๋ธ ๋ก๋ | |
| print("CosyVoice2 ๋ชจ๋ธ ๋ก๋ ์ค...") | |
| cosyvoice_model = CosyVoice2( | |
| model_path, | |
| load_jit=False, | |
| load_trt=False, | |
| fp16=False, | |
| ) | |
| # ์์ ๋๋ ํ ๋ฆฌ ๋ณต์ | |
| os.chdir(original_cwd) | |
| print("โ CosyVoice2 ๋ชจ๋ธ ์ด๊ธฐํ ์๋ฃ!") | |
| return True | |
| except Exception as e: | |
| # ์์ ๋๋ ํ ๋ฆฌ ๋ณต์ | |
| try: | |
| os.chdir(original_cwd) | |
| except: | |
| pass | |
| print(f"โ ๋ชจ๋ธ ์ด๊ธฐํ ์คํจ: {str(e)}") | |
| traceback.print_exc() | |
| return False | |
| # ---------------- ์๋ฒ ์์ ์ ๋ชจ๋ธ ์ด๊ธฐํ ---------------- | |
| from contextlib import asynccontextmanager | |
| async def lifespan(app: FastAPI): | |
| """์๋ฒ ์์ ์ ๋ชจ๋ธ์ ์ด๊ธฐํํฉ๋๋ค.""" | |
| print("๐ ์๋ฒ ์์ - ๋ชจ๋ธ ์ด๊ธฐํ ์ค...") | |
| initialize_cosyvoice() | |
| yield | |
| # FastAPI ์ฑ์ lifespan ์ ์ฉ | |
| app = FastAPI( | |
| title="CosyVoice2 Korean TTS API", | |
| description="FastAPI + CosyVoice2 ๊ธฐ๋ฐ ํ๊ตญ์ด ์์ฑ ํฉ์ฑ ์๋ฒ", | |
| version="1.0.0", | |
| lifespan=lifespan | |
| ) | |
| # ---------------- ์ ๋ ฅ/์ถ๋ ฅ ๋ชจ๋ธ ---------------- | |
| class TTSRequest(BaseModel): | |
| text: str | |
| prompt_text: str | |
| class TTSResponse(BaseModel): | |
| status: str | |
| message: str | |
| audio_path: str = None | |
| # ---------------- API: JSON POST ---------------- | |
| async def synthesize_speech(request: TTSRequest, prompt_audio: UploadFile = File(...)): | |
| """ | |
| ์์ฑ ํฉ์ฑ API | |
| - text: ํฉ์ฑํ ํ ์คํธ | |
| - prompt_text: ํ๋กฌํํธ ์์ฑ์ ํ ์คํธ | |
| - prompt_audio: ํ๋กฌํํธ ์์ฑ ํ์ผ (wav, mp3, flac ๋ฑ) | |
| """ | |
| if cosyvoice_model is None: | |
| return TTSResponse( | |
| status="error", | |
| message="๋ชจ๋ธ์ด ์ด๊ธฐํ๋์ง ์์์ต๋๋ค. ์๋ฒ ๋ก๊ทธ๋ฅผ ํ์ธํด์ฃผ์ธ์." | |
| ) | |
| try: | |
| # ์์ ํ์ผ๋ก ํ๋กฌํํธ ์์ฑ ์ ์ฅ (ํ์ฅ์ ์ ์ง) | |
| temp_file_extension = os.path.splitext(prompt_audio.filename)[1].lower() | |
| if not temp_file_extension: | |
| temp_file_extension = '.wav' # ๊ธฐ๋ณธ๊ฐ | |
| with tempfile.NamedTemporaryFile(delete=False, suffix=temp_file_extension) as temp_file: | |
| temp_file.write(await prompt_audio.read()) | |
| temp_path = temp_file.name | |
| # ํ๋กฌํํธ ์์ฑ ๋ก๋ (16kHz) | |
| try: | |
| prompt_speech_16k = load_wav(temp_path, 16000) | |
| except Exception as e: | |
| print(f"load_wav ์คํจ: {e}") | |
| # fallback: librosa ์ง์ ์ฌ์ฉ | |
| import librosa | |
| import torch | |
| audio_data, sr = librosa.load(temp_path, sr=16000) | |
| prompt_speech_16k = torch.from_numpy(audio_data).unsqueeze(0) | |
| # ์์ฑ ํฉ์ฑ ์คํ | |
| results_generator = cosyvoice_model.inference_zero_shot( | |
| request.text, | |
| prompt_text=request.prompt_text, | |
| prompt_speech_16k=prompt_speech_16k, | |
| text_frontend=True | |
| ) | |
| # generator๋ฅผ ๋ฆฌ์คํธ๋ก ๋ณํ | |
| results = list(results_generator) | |
| if not results: | |
| return TTSResponse( | |
| status="error", | |
| message="์์ฑ ํฉ์ฑ ๊ฒฐ๊ณผ๊ฐ ๋น์ด์์ต๋๋ค." | |
| ) | |
| # ๊ฒฐ๊ณผ ์ ์ฅ (์ถ๋ ฅ ๋๋ ํ ๋ฆฌ ์ง์ ) | |
| output_dir = '/app/outputs' | |
| os.makedirs(output_dir, exist_ok=True) | |
| output_filename = f'output_{hash(request.text)}.wav' | |
| output_path = os.path.join(output_dir, output_filename) | |
| torchaudio.save(output_path, results[0]['tts_speech'], cosyvoice_model.sample_rate) | |
| # ์์ ํ์ผ ์ ๋ฆฌ | |
| os.unlink(temp_path) | |
| return TTSResponse( | |
| status="success", | |
| message="์์ฑ ํฉ์ฑ์ด ์๋ฃ๋์์ต๋๋ค.", | |
| audio_path=f'outputs/{output_filename}' | |
| ) | |
| except Exception as e: | |
| return TTSResponse( | |
| status="error", | |
| message=f"์์ฑ ํฉ์ฑ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค: {str(e)}" | |
| ) | |
| # ---------------- ์ค๋์ค ํ์ผ ๋ค์ด๋ก๋ ---------------- | |
| async def download_audio(filepath: str): | |
| """ํฉ์ฑ๋ ์ค๋์ค ํ์ผ์ ๋ค์ด๋ก๋ํฉ๋๋ค.""" | |
| full_path = os.path.join('/app', filepath) | |
| if os.path.exists(full_path): | |
| filename = os.path.basename(filepath) | |
| return FileResponse(full_path, media_type="audio/wav", filename=filename) | |
| else: | |
| return {"error": "ํ์ผ์ ์ฐพ์ ์ ์์ต๋๋ค."} | |
| # ---------------- HTML UI ---------------- | |
| async def main_ui(): | |
| return """ | |
| <html> | |
| <head> | |
| <title>CosyVoice2 Korean TTS</title> | |
| <meta charset="UTF-8"> | |
| <style> | |
| body { | |
| font-family: 'Segoe UI', Arial, sans-serif; | |
| max-width: 900px; | |
| margin: auto; | |
| padding: 2rem; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| min-height: 100vh; | |
| } | |
| .container { | |
| background-color: white; | |
| padding: 2.5rem; | |
| border-radius: 15px; | |
| box-shadow: 0 10px 30px rgba(0,0,0,0.2); | |
| } | |
| .header { | |
| text-align: center; | |
| margin-bottom: 2rem; | |
| } | |
| .header h1 { | |
| color: #333; | |
| margin-bottom: 0.5rem; | |
| } | |
| .header p { | |
| color: #666; | |
| font-size: 1.1rem; | |
| } | |
| .form-group { | |
| margin-bottom: 1.5rem; | |
| } | |
| label { | |
| display: block; | |
| margin-bottom: 0.5rem; | |
| font-weight: 600; | |
| color: #333; | |
| } | |
| input[type="text"], textarea { | |
| width: 100%; | |
| padding: 0.75rem; | |
| border: 2px solid #e0e0e0; | |
| border-radius: 8px; | |
| font-size: 1rem; | |
| box-sizing: border-box; | |
| transition: border-color 0.3s; | |
| } | |
| input[type="text"]:focus, textarea:focus { | |
| outline: none; | |
| border-color: #667eea; | |
| } | |
| input[type="file"] { | |
| width: 100%; | |
| padding: 0.75rem; | |
| border: 2px dashed #ccc; | |
| border-radius: 8px; | |
| background-color: #f9f9f9; | |
| box-sizing: border-box; | |
| transition: all 0.3s; | |
| } | |
| input[type="file"]:hover { | |
| border-color: #667eea; | |
| background-color: #f0f4ff; | |
| } | |
| input[type="submit"] { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| padding: 1rem 2rem; | |
| border: none; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| font-size: 1.1rem; | |
| font-weight: 600; | |
| transition: transform 0.2s; | |
| width: 100%; | |
| } | |
| input[type="submit"]:hover { | |
| transform: translateY(-2px); | |
| } | |
| .info { | |
| background: linear-gradient(135deg, #e3f2fd 0%, #f3e5f5 100%); | |
| padding: 1.5rem; | |
| border-radius: 10px; | |
| margin-bottom: 2rem; | |
| border-left: 4px solid #667eea; | |
| } | |
| .example { | |
| background-color: #f8f9fa; | |
| padding: 1rem; | |
| border-radius: 8px; | |
| margin-top: 0.5rem; | |
| border-left: 3px solid #28a745; | |
| } | |
| .example strong { | |
| color: #28a745; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header"> | |
| <h1>๐ค CosyVoice2 ์์ฑ ํฉ์ฑ๊ธฐ</h1> | |
| <p>ํ๊ตญ์ด ํ ์คํธ๋ฅผ ์์ฐ์ค๋ฌ์ด ์์ฑ์ผ๋ก ๋ณํํด๋ณด์ธ์!</p> | |
| </div> | |
| <div class="info"> | |
| <strong>๐ ์ฌ์ฉ ๋ฐฉ๋ฒ:</strong><br> | |
| 1. <strong>ํ๋กฌํํธ ์์ฑ:</strong> ๋ชฉ์๋ฆฌ ์คํ์ผ์ ๊ธฐ์ค์ด ๋ ์์ฑ ํ์ผ์ ์ ๋ก๋ํ์ธ์<br> | |
| 2. <strong>ํ๋กฌํํธ ํ ์คํธ:</strong> ์ ๋ก๋ํ ์์ฑ์ ์ค์ ๋ด์ฉ์ ์ ๋ ฅํ์ธ์<br> | |
| 3. <strong>ํฉ์ฑํ ํ ์คํธ:</strong> ์๋ก ์์ฑํ๊ณ ์ถ์ ์์ฑ์ ํ ์คํธ๋ฅผ ์ ๋ ฅํ์ธ์<br><br> | |
| <strong>์ง์ ํ์:</strong> WAV | |
| </div> | |
| <form action="/submit" method="post" enctype="multipart/form-data"> | |
| <div class="form-group"> | |
| <label for="prompt_audio">๐ต ํ๋กฌํํธ ์์ฑ ํ์ผ:</label> | |
| <input type="file" id="prompt_audio" name="prompt_audio" accept=".wav" required> | |
| <div class="example"> | |
| <strong>์์:</strong> "์๋ ํ์ธ์"๋ผ๊ณ ๋งํ๋ ์์ฑ ํ์ผ | |
| </div> | |
| </div> | |
| <div class="form-group"> | |
| <label for="prompt_text">๐ ํ๋กฌํํธ ํ ์คํธ:</label> | |
| <input type="text" id="prompt_text" name="prompt_text" | |
| placeholder="์ ๋ก๋ํ ์์ฑ์ ์ค์ ๋ด์ฉ" | |
| value="์๋ ํ์ธ์" required> | |
| <div class="example"> | |
| <strong>์์:</strong> ์๋ ํ์ธ์ (์ ๋ก๋ํ ์์ฑ ํ์ผ์ ์ค์ ๋ด์ฉ) | |
| </div> | |
| </div> | |
| <div class="form-group"> | |
| <label for="text">๐ฏ ํฉ์ฑํ ํ ์คํธ:</label> | |
| <textarea id="text" name="text" rows="3" | |
| placeholder="์๋ก ์์ฑํ๊ณ ์ถ์ ์์ฑ์ ํ ์คํธ๋ฅผ ์ ๋ ฅํ์ธ์" | |
| required>๊ณต๋ฃก์ด ๋ฐค์๊ฐฑ์ ๋ชฐ๋ ๋จน๊ณ ๋๋ง์ณค์ด์.</textarea> | |
| <div class="example"> | |
| <strong>์์:</strong> ๊ณต๋ฃก์ด ๋ฐค์๊ฐฑ์ ๋ชฐ๋ ๋จน๊ณ ๋๋ง์ณค์ด์. | |
| </div> | |
| </div> | |
| <input type="submit" value="๐ ์์ฑ ํฉ์ฑ ์์"> | |
| </form> | |
| </div> | |
| </body> | |
| </html> | |
| """ | |
| # ---------------- ๊ฒฐ๊ณผ ๋ ๋๋ง ---------------- | |
| async def handle_form( | |
| request: Request, | |
| text: str = Form(...), | |
| prompt_text: str = Form(...), | |
| prompt_audio: UploadFile = File(...) | |
| ): | |
| try: | |
| if cosyvoice_model is None: | |
| return """ | |
| <html> | |
| <head><title>์๋ฌ</title><meta charset="UTF-8"></head> | |
| <body style="font-family: Arial, sans-serif; max-width: 600px; margin: auto; padding: 2rem;"> | |
| <h1>โ ๋ชจ๋ธ ์ด๊ธฐํ ์ค๋ฅ</h1> | |
| <p>CosyVoice2 ๋ชจ๋ธ์ด ์์ง ์ด๊ธฐํ๋์ง ์์์ต๋๋ค.</p> | |
| <p>์๋ฒ ๋ก๊ทธ๋ฅผ ํ์ธํ๊ณ ์ ์ ํ ๋ค์ ์๋ํด์ฃผ์ธ์.</p> | |
| <br> | |
| <a href="/" style="color: #667eea; text-decoration: none;">โ ๋์๊ฐ๊ธฐ</a> | |
| </body> | |
| </html> | |
| """ | |
| # ํ์ผ ํ์ ๊ฒ์ฆ | |
| if not prompt_audio.filename.lower().endswith('.wav'): | |
| return """ | |
| <html> | |
| <head><title>์๋ฌ</title><meta charset="UTF-8"></head> | |
| <body style="font-family: Arial, sans-serif; max-width: 600px; margin: auto; padding: 2rem;"> | |
| <h1>โ ํ์ผ ํ์ ์ค๋ฅ</h1> | |
| <p>WAV ํ์ผ๋ง ์ง์๋ฉ๋๋ค.</p> | |
| <p><strong>์ง์ ํ์:</strong> WAV</p> | |
| <br> | |
| <a href="/" style="color: #667eea; text-decoration: none;">โ ๋์๊ฐ๊ธฐ</a> | |
| </body> | |
| </html> | |
| """ | |
| # ์์ ํ์ผ๋ก ํ๋กฌํํธ ์์ฑ ์ ์ฅ | |
| temp_file_extension = os.path.splitext(prompt_audio.filename)[1].lower() | |
| if not temp_file_extension: | |
| temp_file_extension = '.wav' # ๊ธฐ๋ณธ๊ฐ | |
| with tempfile.NamedTemporaryFile(delete=False, suffix=temp_file_extension) as temp_file: | |
| temp_file.write(await prompt_audio.read()) | |
| temp_path = temp_file.name | |
| print(f"์ ๋ก๋๋ ํ์ผ: {prompt_audio.filename}") | |
| print(f"์์ ํ์ผ ๊ฒฝ๋ก: {temp_path}") | |
| print(f"ํ์ผ ํฌ๊ธฐ: {os.path.getsize(temp_path)} bytes") | |
| # ํ๋กฌํํธ ์์ฑ ๋ก๋ (16kHz) - ๋ ์์ ํ ๋ฐฉ๋ฒ์ผ๋ก | |
| try: | |
| prompt_speech_16k = load_wav(temp_path, 16000) | |
| print(f"์ค๋์ค ๋ก๋ ์ฑ๊ณต: shape={prompt_speech_16k.shape}") | |
| except Exception as e: | |
| print(f"load_wav ์คํจ: {e}") | |
| # fallback: librosa ์ง์ ์ฌ์ฉ | |
| import librosa | |
| import torch | |
| audio_data, sr = librosa.load(temp_path, sr=16000) | |
| prompt_speech_16k = torch.from_numpy(audio_data).unsqueeze(0) | |
| print(f"librosa fallback ์ฑ๊ณต: shape={prompt_speech_16k.shape}") | |
| # ์์ฑ ํฉ์ฑ ์คํ | |
| print(f"์์ฑ ํฉ์ฑ ์์: text='{text}', prompt_text='{prompt_text}'") | |
| results_generator = cosyvoice_model.inference_zero_shot( | |
| text, | |
| prompt_text=prompt_text, | |
| prompt_speech_16k=prompt_speech_16k, | |
| text_frontend=True | |
| ) | |
| # generator๋ฅผ ๋ฆฌ์คํธ๋ก ๋ณํ | |
| results = list(results_generator) | |
| print(f"์์ฑ ํฉ์ฑ ์๋ฃ! ๊ฒฐ๊ณผ ๊ฐ์: {len(results)}") | |
| if not results: | |
| raise Exception("์์ฑ ํฉ์ฑ ๊ฒฐ๊ณผ๊ฐ ๋น์ด์์ต๋๋ค.") | |
| # ๊ฒฐ๊ณผ ์ ์ฅ (์ถ๋ ฅ ๋๋ ํ ๋ฆฌ ์ง์ ) | |
| output_dir = '/app/outputs' | |
| os.makedirs(output_dir, exist_ok=True) | |
| output_filename = f'korean_tts_output_{hash(text)}.wav' | |
| output_path = os.path.join(output_dir, output_filename) | |
| torchaudio.save(output_path, results[0]['tts_speech'], cosyvoice_model.sample_rate) | |
| print(f"์ค๋์ค ํ์ผ ์ ์ฅ ์๋ฃ: {output_path}") | |
| # ๋ค์ด๋ก๋์ฉ ์๋ ๊ฒฝ๋ก | |
| download_filename = f'outputs/{output_filename}' | |
| # ์์ ํ์ผ ์ ๋ฆฌ | |
| os.unlink(temp_path) | |
| except Exception as e: | |
| error_details = traceback.format_exc() | |
| return f""" | |
| <html> | |
| <head><title>์๋ฌ</title><meta charset="UTF-8"></head> | |
| <body style="font-family: Arial, sans-serif; max-width: 700px; margin: auto; padding: 2rem;"> | |
| <h1>โ ์๋ฒ ์ค๋ฅ ๋ฐ์</h1> | |
| <p><strong>์ค๋ฅ ๋ฉ์์ง:</strong></p> | |
| <pre style="background-color: #f8f9fa; padding: 1rem; border-radius: 5px; overflow-x: auto;">{str(e)}</pre> | |
| <hr> | |
| <details> | |
| <summary><strong>์๋ฌ ์์ธ (ํด๋ฆญํ์ฌ ํผ์น๊ธฐ)</strong></summary> | |
| <pre style="background-color: #f8f9fa; padding: 1rem; border-radius: 5px; overflow-x: auto;">{error_details}</pre> | |
| </details> | |
| <br> | |
| <a href="/" style="color: #667eea; text-decoration: none;">โ ๋์๊ฐ๊ธฐ</a> | |
| </body> | |
| </html> | |
| """ | |
| return f""" | |
| <html> | |
| <head><title>ํฉ์ฑ ๊ฒฐ๊ณผ</title><meta charset="UTF-8"></head> | |
| <body style="font-family: Arial, sans-serif; max-width: 700px; margin: auto; padding: 2rem;"> | |
| <h1>โ ์์ฑ ํฉ์ฑ ์๋ฃ!</h1> | |
| <div style="background: linear-gradient(135deg, #e3f2fd 0%, #f3e5f5 100%); padding: 1.5rem; border-radius: 10px; margin: 1.5rem 0;"> | |
| <h3>๐ ์ ๋ ฅ ์ ๋ณด</h3> | |
| <p><strong>ํ๋กฌํํธ ์์ฑ:</strong> {prompt_audio.filename}</p> | |
| <p><strong>ํ๋กฌํํธ ํ ์คํธ:</strong> {prompt_text}</p> | |
| <p><strong>ํฉ์ฑํ ํ ์คํธ:</strong> {text}</p> | |
| </div> | |
| <div style="background-color: #f8f9fa; padding: 1.5rem; border-radius: 10px; border-left: 4px solid #28a745;"> | |
| <h3>๐ต ํฉ์ฑ๋ ์์ฑ</h3> | |
| <audio controls style="width: 100%; margin: 1rem 0;"> | |
| <source src="/download/{download_filename}" type="audio/wav"> | |
| ๋ธ๋ผ์ฐ์ ๊ฐ ์ค๋์ค๋ฅผ ์ง์ํ์ง ์์ต๋๋ค. | |
| </audio> | |
| <br> | |
| <a href="/download/{download_filename}" | |
| style="background: linear-gradient(135deg, #28a745 0%, #20c997 100%); | |
| color: white; padding: 0.75rem 1.5rem; text-decoration: none; | |
| border-radius: 8px; display: inline-block; margin-top: 1rem;"> | |
| ๐ฅ ํ์ผ ๋ค์ด๋ก๋ | |
| </a> | |
| </div> | |
| <br> | |
| <a href="/" style="color: #667eea; text-decoration: none; font-size: 1.1rem;">โ ๋ค์ ์๋ํ๊ธฐ</a> | |
| </body> | |
| </html> | |
| """ | |
| # ---------------- ํฌ์ค ์ฒดํฌ ---------------- | |
| async def health_check(): | |
| return { | |
| "status": "ok" if cosyvoice_model is not None else "initializing", | |
| "model_loaded": cosyvoice_model is not None, | |
| "description": "CosyVoice2 Korean TTS Server" | |
| } | |
| if __name__ == "__main__": | |
| import uvicorn | |
| uvicorn.run(app, host="0.0.0.0", port=7860) |