Spaces:
Build error
Build error
Download app.py from speako/cosyvoice2-server: direct link, hf CLI and curl.
- Browser
- Download file 21.6 kB
-
https://huggingface.co/spaces/speako/cosyvoice2-server/resolve/main/app.py
- Command line
-
hf download hf://spaces/speako/cosyvoice2-server/app.py
-
curl -L -o app.py https://huggingface.co/spaces/speako/cosyvoice2-server/resolve/main/app.py
21.6 kB
| # 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) |