From 49576c142e89ec08d4c5466ef7ce6b7d4ec9611d Mon Sep 17 00:00:00 2001 From: ehlxr Date: Wed, 23 Apr 2025 13:42:45 +0800 Subject: [PATCH] feat(mcp/speech): add speech mcp server --- mcp/server/mcp_server_speech/.python-version | 1 + mcp/server/mcp_server_speech/README.md | 91 +++ mcp/server/mcp_server_speech/pyproject.toml | 45 ++ .../src/mcp_server_speech/__init__.py | 0 .../src/mcp_server_speech/__main__.py | 4 + .../src/mcp_server_speech/config.py | 61 ++ .../src/mcp_server_speech/main.py | 33 + .../src/mcp_server_speech/models.py | 51 ++ .../src/mcp_server_speech/server.py | 105 +++ .../mcp_server_speech/services/__init__.py | 0 .../src/mcp_server_speech/services/asr.py | 212 ++++++ .../src/mcp_server_speech/services/asr_ws.py | 604 ++++++++++++++++++ .../src/mcp_server_speech/services/tts.py | 157 +++++ mcp/server/mcp_server_speech/uv.lock | 481 ++++++++++++++ 14 files changed, 1845 insertions(+) create mode 100644 mcp/server/mcp_server_speech/.python-version create mode 100644 mcp/server/mcp_server_speech/README.md create mode 100644 mcp/server/mcp_server_speech/pyproject.toml create mode 100644 mcp/server/mcp_server_speech/src/mcp_server_speech/__init__.py create mode 100644 mcp/server/mcp_server_speech/src/mcp_server_speech/__main__.py create mode 100644 mcp/server/mcp_server_speech/src/mcp_server_speech/config.py create mode 100644 mcp/server/mcp_server_speech/src/mcp_server_speech/main.py create mode 100644 mcp/server/mcp_server_speech/src/mcp_server_speech/models.py create mode 100644 mcp/server/mcp_server_speech/src/mcp_server_speech/server.py create mode 100644 mcp/server/mcp_server_speech/src/mcp_server_speech/services/__init__.py create mode 100644 mcp/server/mcp_server_speech/src/mcp_server_speech/services/asr.py create mode 100644 mcp/server/mcp_server_speech/src/mcp_server_speech/services/asr_ws.py create mode 100644 mcp/server/mcp_server_speech/src/mcp_server_speech/services/tts.py create mode 100644 mcp/server/mcp_server_speech/uv.lock diff --git a/mcp/server/mcp_server_speech/.python-version b/mcp/server/mcp_server_speech/.python-version new file mode 100644 index 00000000..e4fba218 --- /dev/null +++ b/mcp/server/mcp_server_speech/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/mcp/server/mcp_server_speech/README.md b/mcp/server/mcp_server_speech/README.md new file mode 100644 index 00000000..b076530c --- /dev/null +++ b/mcp/server/mcp_server_speech/README.md @@ -0,0 +1,91 @@ +# Speech Model Context Protocol Server + +An MCP server implementation for speech of volcengine + +## Features + +### Tools + +- **asr** + Automatic Speech Recognition: Converts audio to text. + - Args: + - content: url or absolute path of the audio file to transcribe. + - Returns: + - Asr text +- **tts** + Text-to-Speech: Synthesizes text into audio. + - Args: + - text: The text to synthesize into speech. + - speed: Speech speed (e.g., 1.0 for normal). default: 1.0. + - encoding: Desired audio output format (e.g., 'mp3', 'wav'). default: 'mp3'. + - Returns: + - Return the path of audio file. + +## Configuration + +The server requires the following environment variables to be set: + +- `VOLC_APPID`: Required, The APP ID for the VolcEngine. +- `VOLC_TOKEN`: Required, The Access Token for the VolcEngine. +- `VOLC_VOICE_TYPE`: Optional, Large speech synthesis model service voice_type, default is 'zh_female_meilinvyou_moon_bigtts' +- `VOLC_CLUSTER`: Required, Large speech synthesis model service cluster ID + +The services that need to be activated on Volcengine are: [Large speech synthesis model](https://console.volcengine.com/speech/service/10007)、[Streaming speech recognition large model](< model for audio file recognition>]() + +You can set these environment variables in your shell. + +### MCP Settings Configuration + +To add this server to your MCP configuration, add the following to your MCP settings file: + +```json +{ + "mcpServers": { + "speech-mcp-server": { + "command": "uv", + "args": [ + "--directory", + "/ABSOLUTE/PATH/TO/PARENT/FOLDER/src/mcp_server_speech", + "run", + "main.py" + ] + } + } +} +``` + +or + +```json +{ + "mcpServers": { + "speech-mcp-server": { + "command": "uvx", + "args": [ + "--from", + "git+https://github.com/volcengine/ai-app-lab#subdirectory=mcp/server/mcp_server_speech", + "mcp-server-speech" + ], + "env": { + "VOLC_APPID": "your appid", + "VOLC_TOKEN": "your token", + "VOLC_VOICE_TYPE": "tts voice type", + "VOLC_CLUSTER": "tts cluster id", + } + } + } +} +``` + +## Usage + +### Running the Server + +```bash +# Run the server with stdio transport (default) +python -m mcp_server_speech [--transport/-t {sse,stdio}] +``` + +## License + +This library is licensed under the MIT-0 License. See the LICENSE file. diff --git a/mcp/server/mcp_server_speech/pyproject.toml b/mcp/server/mcp_server_speech/pyproject.toml new file mode 100644 index 00000000..34cc4249 --- /dev/null +++ b/mcp/server/mcp_server_speech/pyproject.toml @@ -0,0 +1,45 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "mcp-server-speech" +version = "0.1.0" +description = "An MCP server providing speech-related tools like ASR and TTS." +readme = "README.md" +requires-python = ">=3.12" +license = { text = "MIT" } +authors = [{ name = "lixiangrong", email = "xiagnrong.li@thundercomm.com" }] +dependencies = [ + "aiofiles>=24.1.0", + "mcp[cli]>=1.6.0", + "pydub>=0.25.1", + "python-dotenv>=1.1.0", + "requests>=2.32.3", + "websockets<14", +] + +[project.scripts] +mcp-server-speech = "mcp_server_speech.main:main" + +[tool.hatch.metadata] +allow-direct-references = true + +[tool.ruff] +# Add ruff linting/formatting configurations here +line-length = 88 +lint.select = [ + "E", + "F", + "W", + "I", + "N", + "UP", + "B", + "A", + "C4", + "T20", + "SIM", + "PTH", +] +lint.ignore = ["E501"] # Ignore line length errors if needed diff --git a/mcp/server/mcp_server_speech/src/mcp_server_speech/__init__.py b/mcp/server/mcp_server_speech/src/mcp_server_speech/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/mcp/server/mcp_server_speech/src/mcp_server_speech/__main__.py b/mcp/server/mcp_server_speech/src/mcp_server_speech/__main__.py new file mode 100644 index 00000000..29025226 --- /dev/null +++ b/mcp/server/mcp_server_speech/src/mcp_server_speech/__main__.py @@ -0,0 +1,4 @@ +from mcp_server_speech.main import main # Updated import path + +if __name__ == "__main__": + main() diff --git a/mcp/server/mcp_server_speech/src/mcp_server_speech/config.py b/mcp/server/mcp_server_speech/src/mcp_server_speech/config.py new file mode 100644 index 00000000..12c23772 --- /dev/null +++ b/mcp/server/mcp_server_speech/src/mcp_server_speech/config.py @@ -0,0 +1,61 @@ +import logging +import os +from pathlib import Path + +log_dir = Path(__file__).resolve().parent.parent.parent / "logs" +log_dir.mkdir(exist_ok=True) + +log_formatter = logging.Formatter( + "%(asctime)s - %(name)s - [%(filename)s:%(lineno)d]- %(levelname)s - %(message)s" +) + +console_handler = logging.StreamHandler() +console_handler.setFormatter(log_formatter) + +# Create file handler +log_file = os.getenv("LOG_FILE_PATH", str(log_dir / "speech.log")) +file_handler = logging.FileHandler(log_file, encoding="utf-8") +file_handler.setFormatter(log_formatter) + +# Configure root logger +root_logger = logging.getLogger() +root_logger.setLevel(logging.INFO) +root_logger.addHandler(console_handler) + +# Enable file logging based on environment variable +if os.getenv("ENABLE_FILE_LOGGING", "false").lower() == "true": + root_logger.addHandler(file_handler) + +logger = logging.getLogger(__name__) + +# load environment variables + +VOLC_APPID = None +VOLC_TOKEN = None +VOLC_CLUSTER = None +VOLC_VOICE_TYPE = None + + +def load_config(): + global VOLC_APPID, VOLC_TOKEN, VOLC_CLUSTER, VOLC_VOICE_TYPE + + VOLC_APPID = os.getenv("VOLC_APPID") + logger.info(f"VOLC_APPID loaded: {VOLC_APPID}") # Log loaded value + VOLC_TOKEN = os.getenv("VOLC_TOKEN") + logger.info(f"VOLC_TOKEN loaded: {VOLC_TOKEN}") # Log loaded value + VOLC_CLUSTER = os.getenv("VOLC_CLUSTER") + logger.info(f"VOLC_CLUSTER loaded: {VOLC_CLUSTER}") # Log loaded value + VOLC_VOICE_TYPE = os.getenv("VOLC_VOICE_TYPE", "zh_female_meilinvyou_moon_bigtts") + logger.info(f"VOLC_VOICE_TYPE loaded: {VOLC_VOICE_TYPE}") # Log loaded value + + # Check if required environment variables are set + if not all([VOLC_APPID, VOLC_TOKEN, VOLC_CLUSTER]): + logger.error( + "Missing required environment variables: VOLC_APPID, VOLC_TOKEN, VOLC_CLUSTER" + ) + raise ValueError( + "Missing required environment variables: VOLC_APPID, VOLC_TOKEN, VOLC_CLUSTER" + ) + + +load_config() diff --git a/mcp/server/mcp_server_speech/src/mcp_server_speech/main.py b/mcp/server/mcp_server_speech/src/mcp_server_speech/main.py new file mode 100644 index 00000000..4021f0b1 --- /dev/null +++ b/mcp/server/mcp_server_speech/src/mcp_server_speech/main.py @@ -0,0 +1,33 @@ +import argparse +import logging + +from mcp_server_speech.server import mcp + +logger = logging.getLogger(__name__) + + +def main(): + parser = argparse.ArgumentParser(description="Run the Speech MCP Server") + parser.add_argument( + "--transport", + "-t", + choices=["sse", "stdio"], + default="stdio", + help="Transport protocol to use (sse or stdio)", + ) + + args = parser.parse_args() + mcp.transport = args.transport # Store transport on mcp object + + try: + logger.info(f"Starting Speech MCP Server with transport: {mcp.transport}") + mcp.run(transport=mcp.transport) + except KeyboardInterrupt: + logger.info("Speech MCP Server stopped by user.") + except Exception as e: + logger.error(f"Error starting Speech MCP Server: {str(e)}") + raise + + +if __name__ == "__main__": + main() diff --git a/mcp/server/mcp_server_speech/src/mcp_server_speech/models.py b/mcp/server/mcp_server_speech/src/mcp_server_speech/models.py new file mode 100644 index 00000000..24cb05b2 --- /dev/null +++ b/mcp/server/mcp_server_speech/src/mcp_server_speech/models.py @@ -0,0 +1,51 @@ +from enum import Enum + +from pydantic import BaseModel, Field + +# --- Pydantic Models for Tool Inputs/Outputs --- + + +class AudioSourceType(Enum): + URL = "url" + FILE = "file" + + +class AsrInputArgs(BaseModel): + """Arguments for the ASR tool.""" + + source: str = Field( + ..., description="Path to the audio file or URL of the audio stream." + ) + source_type: AudioSourceType = Field( + ..., description="Type of the audio source (e.g., 'file', 'url')." + ) + options: dict | None = Field( + None, description="Additional options for ASR processing." + ) + + +class AsrOutputResult(BaseModel): + """Output of the ASR tool.""" + + text: str = Field(..., description="The recognized text from the audio.") + + +class TtsInputArgs(BaseModel): + """Arguments for the TTS tool.""" + + text: str = Field(..., description="The text to synthesize into speech.") + speed: float = Field(1.0, description="Speech speed (e.g., 1.0 for normal).") + encoding: str = Field( + "mp3", description="Desired audio output format (e.g., 'mp3', 'wav')." + ) + + +class TtsOutputResult(BaseModel): + """Output of the TTS tool.""" + + format: str = Field( + ..., description="The format of the audio data (e.g., 'mp3', 'wav')." + ) + file_path: str = Field( + "", description="The path to the saved audio file (if applicable)." + ) diff --git a/mcp/server/mcp_server_speech/src/mcp_server_speech/server.py b/mcp/server/mcp_server_speech/src/mcp_server_speech/server.py new file mode 100644 index 00000000..b77f13ac --- /dev/null +++ b/mcp/server/mcp_server_speech/src/mcp_server_speech/server.py @@ -0,0 +1,105 @@ +import logging +import os + +from mcp.server.fastmcp import FastMCP +from pydantic import Field + +from mcp_server_speech.models import ( + AsrInputArgs, + TtsInputArgs, + TtsOutputResult, +) +from mcp_server_speech.services.asr import AsrService, AudioSourceType +from mcp_server_speech.services.tts import tts_request_handler + +# Initialize FastMCP server +mcp = FastMCP("Speech MCP Server", port=int(os.getenv("PORT", "8000"))) + +logger = logging.getLogger(__name__) + + +@mcp.tool() +async def tts( + text: str = Field(..., description="The text to synthesize into speech."), + speed: float = Field( + 1.0, description="Speech speed (e.g., 1.0 for normal). default: 1.0." + ), + encoding: str = Field( + "mp3", + description="Desired audio output format (e.g., 'mp3', 'wav'). default: 'mp3'.", + ), +) -> TtsOutputResult: + """ + Text-to-Speech: Synthesizes text into audio. + Return the path of audio file. + """ + + # Parameter validation logic + if not text or text.strip() == "": + raise ValueError("The text parameter cannot be empty.") + if speed <= 0: + raise ValueError("Speed must be a positive value.") + if encoding not in ("mp3", "wav"): + raise ValueError("Encoding must be either 'mp3' or 'wav'.") + + try: + result = await tts_request_handler( + TtsInputArgs(text=text, speed=speed, encoding=encoding) + ) + + return result + except ValueError as e: + logging.error(f"Value error in Text to Speech: {e}") + raise + except TimeoutError as e: + logging.error(f"Timeout error in Text to Speech: {e}") + raise + except Exception as e: + logging.error(f"Error Text to Speech: {e}") + raise + + +@mcp.tool() +async def asr( + content: str = Field( + ..., + description="url or absolute path of the audio file to transcribe.", + ), +) -> str: + """ + Automatic Speech Recognition: Converts audio to text. + """ + + # Parameter validation logic + if not content or content.strip() == "": + raise ValueError("The content parameter cannot be empty.") + + try: + service = AsrService() + source_type = service.detect_source_type(content) # Detect source type + result = None + options = None + + if mcp.transport == "sse" and source_type == AudioSourceType.FILE: + return "Error: SSE transport does not support file input." + + if mcp.transport == "stdio": + options = {"format": "mp3", "rate": 16000, "channel": 1, "bits": 16} + + result = await service.recognize( + AsrInputArgs(source=content, source_type=source_type, options=options) + ) + + logger.info(f"asr result: {result}") + + return result.text + + except ValueError as e: + logging.error(f"Value error in Automatic Speech Recognition: {e}") + raise + except TimeoutError as e: + logging.error(f"Timeout error in Automatic Speech Recognition: {e}") + raise + except Exception as e: + logging.error(f"Error Automatic Speech Recognition: {e}") + raise diff --git a/mcp/server/mcp_server_speech/src/mcp_server_speech/services/__init__.py b/mcp/server/mcp_server_speech/src/mcp_server_speech/services/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/mcp/server/mcp_server_speech/src/mcp_server_speech/services/asr.py b/mcp/server/mcp_server_speech/src/mcp_server_speech/services/asr.py new file mode 100644 index 00000000..53dbfe7d --- /dev/null +++ b/mcp/server/mcp_server_speech/src/mcp_server_speech/services/asr.py @@ -0,0 +1,212 @@ +import asyncio +import json +import logging +import uuid +from abc import ABC, abstractmethod +from pathlib import Path + +import requests + +from mcp_server_speech.config import VOLC_APPID, VOLC_TOKEN +from mcp_server_speech.models import AsrInputArgs, AsrOutputResult, AudioSourceType +from mcp_server_speech.services.asr_ws import AsrWsClient + +logger = logging.getLogger(__name__) + + +class AsrProcessor(ABC): + """ASR Processor Abstract Base Class""" + + @abstractmethod + async def process(self, source: str, options: dict | None = None) -> dict: + """Process audio and return recognition results""" + pass + + +class UrlAsrProcessor(AsrProcessor): + """URL Audio Processor - Using REST API""" + + async def process(self, source: str, options: dict | None = None) -> dict: + task_id = str(uuid.uuid4()) + submit_result = await self._submit_task(source, task_id) + + if not submit_result.get("success"): + return {"error": submit_result.get("error", "Task submission failed")} + + x_tt_logid = submit_result.get("x_tt_logid", "") + return await self._poll_result(task_id, x_tt_logid) + + async def _submit_task(self, url: str, task_id: str) -> dict: + submit_url = "https://openspeech.bytedance.com/api/v3/auc/bigmodel/submit" + headers = { + "X-Api-App-Key": VOLC_APPID, + "X-Api-Access-Key": VOLC_TOKEN, + "X-Api-Resource-Id": "volc.bigasr.auc", + "X-Api-Request-Id": task_id, + "X-Api-Sequence": "-1", + } + + request = { + "user": {"uid": "fake_uid"}, + "audio": { + "url": url, + "format": "mp3", + }, + "request": { + "model_name": "bigmodel", + }, + } + + try: + response = requests.post( + submit_url, data=json.dumps(request), headers=headers + ) + if ( + "X-Api-Status-Code" in response.headers + and response.headers["X-Api-Status-Code"] == "20000000" + ): + return { + "success": True, + "x_tt_logid": response.headers.get("X-Tt-Logid", ""), + } + else: + return { + "success": False, + "error": f"Task submission failed: {response.content}", + } + except Exception as e: + return {"success": False, "error": f"Task submission exception: {str(e)}"} + + async def _poll_result(self, task_id: str, x_tt_logid: str) -> dict: + query_url = "https://openspeech.bytedance.com/api/v3/auc/bigmodel/query" + headers = { + "X-Api-App-Key": VOLC_APPID, + "X-Api-Access-Key": VOLC_TOKEN, + "X-Api-Resource-Id": "volc.bigasr.auc", + "X-Api-Request-Id": task_id, + "X-Tt-Logid": x_tt_logid, + } + + max_retries = 100 + retry_count = 0 + + while retry_count < max_retries: + try: + response = requests.post(query_url, json.dumps({}), headers=headers) + code = response.headers.get("X-Api-Status-Code", "") + + if code == "20000000": # Task completed + return { + "success": True, + "text": response.json()["result"]["text"], + } + elif code not in ["20000001", "20000002"]: # Task failed + return { + "success": False, + "error": f"Task query failed: {response.content}", + } + + await asyncio.sleep(1) + retry_count += 1 + + except Exception as e: + return {"success": False, "error": f"Task query exception: {str(e)}"} + + return {"success": False, "error": "Task timeout"} + + +class FileAsrProcessor(AsrProcessor): + """Local File Processor - Using WebSocket""" + + async def process(self, source: str, options: dict | None = None) -> dict: + try: + client = AsrWsClient( + audio_path=source, + api_app_key=VOLC_APPID, + api_access_key=VOLC_TOKEN, + **options or {}, + ) + result = await client.execute() + + if "error" in result: + return {"success": False, "error": result["error"]} + + return { + "success": True, + "text": result["payload_msg"]["result"]["text"] + if "payload_msg" in result + else result, + } + + except Exception as e: + return { + "success": False, + "error": f"Local file processing failed: {str(e)}", + } + + +class AsrService: + """Unified ASR Service""" + + def __init__(self): + self._processors = { + AudioSourceType.URL: UrlAsrProcessor(), + AudioSourceType.FILE: FileAsrProcessor(), + } + + def detect_source_type(self, source: str) -> AudioSourceType: + """Detect audio source type""" + if source.startswith(("http://", "https://")): + return AudioSourceType.URL + elif Path(source).exists(): + return AudioSourceType.FILE + else: + raise ValueError(f"Invalid audio source: {source}") + + async def recognize(self, asr_args: AsrInputArgs) -> AsrOutputResult: + """ + Unified speech recognition entry point + + Args: + asr_args (AsrInputArgs): ASR input arguments + + Returns: + AsrOutputResult: Recognition result + """ + # source_type = self.detect_source_type(source) # Detect source type + processor = self._processors[asr_args.source_type] + result = await processor.process(asr_args.source, asr_args.options) + if result.get("success") is False: + raise ValueError(f"Recognition failed: {result.get('error')}") + + return AsrOutputResult( + text=result.get("text"), + ) + + +async def test_asr(): + """Test ASR service""" + service = AsrService() + + # Test URL recognition + url_result = await service.recognize( + asr_args=AsrInputArgs( + source="https://example.com/test.mp3", + source_type=AudioSourceType.URL, + ) + ) + logger.info(f"URL recognition result: {url_result}") + + # Test local file recognition + # file_result = await service.recognize( + # asr_args=AsrInputArgs( + # source="/home/xxx/test.mp3", + # source_type=AudioSourceType.FILE, + # options={"format": "mp3", "rate": 16000, "channel": 1, "bits": 16}, + # ) + # ) + # logger.info(f"Local file recognition result: {file_result}") + + +if __name__ == "__main__": + asyncio.run(test_asr()) diff --git a/mcp/server/mcp_server_speech/src/mcp_server_speech/services/asr_ws.py b/mcp/server/mcp_server_speech/src/mcp_server_speech/services/asr_ws.py new file mode 100644 index 00000000..04bfccf4 --- /dev/null +++ b/mcp/server/mcp_server_speech/src/mcp_server_speech/services/asr_ws.py @@ -0,0 +1,604 @@ +import asyncio +import gzip +import json +import logging +import time +import uuid +import wave +from collections.abc import Generator +from io import BytesIO +from pathlib import Path + +import aiofiles +import websockets + +logger = logging.getLogger(__name__) + + +def parse_headers(headers) -> dict: + """Safely parse headers, handling cases with multiple values.""" + result = {} + if not headers: + return result + + for key in headers: + try: + # If multiple values exist for a header, use the first one + values = headers.get_all(key) + result[key] = values[0] if values else None + except Exception: + # If retrieval fails, skip this header + continue + return result + + +# Protocol version +PROTOCOL_VERSION = 0b0001 +# Message types +FULL_CLIENT_REQUEST = ( + 0b0001 # Full client request (includes audio metadata and parameters) +) +AUDIO_ONLY_REQUEST = 0b0010 # Client audio-only request +FULL_SERVER_RESPONSE = 0b1001 # Full server response (includes recognition results) +SERVER_ACK = 0b1011 # Server acknowledgment message +SERVER_ERROR_RESPONSE = 0b1111 # Server error response + +# Message type specific flags +NO_SEQUENCE = 0b0000 # No sequence check +POS_SEQUENCE = 0b0001 # Positive sequence number (middle data frame) +NEG_WITH_SEQUENCE = 0b0011 # Negative sequence number (end data frame) + +# Message serialization methods +NO_SERIALIZATION = 0b0000 # No serialization +JSON = 0b0001 # JSON serialization + +# Message compression methods +GZIP = 0b0001 # Gzip compression + + +def generate_header( + message_type=FULL_CLIENT_REQUEST, + message_type_specific_flags=NO_SEQUENCE, + serial_method=JSON, + compression_type=GZIP, + reserved_data=0x00, +): + """Generate protocol header. + + Header structure (32 bits / 4 bytes): + - protocol_version (4 bits): Protocol version + - header_size (4 bits): Header size (in 4-byte units) + - message_type (4 bits): Message type + - message_type_specific_flags (4 bits): Message type specific flags + - serialization_method (4 bits): Serialization method + - message_compression (4 bits): Message compression method + - reserved (8 bits): Reserved field + """ + header = bytearray() + header_size = 1 # Header size fixed at 1 unit (4 bytes) + header.append((PROTOCOL_VERSION << 4) | header_size) + header.append((message_type << 4) | message_type_specific_flags) + header.append((serial_method << 4) | compression_type) + header.append(reserved_data) + return header + + +def generate_before_payload(sequence: int): + """Generate sequence number section before payload (if needed).""" + before_payload = bytearray() + before_payload.extend( + sequence.to_bytes(4, "big", signed=True) + ) # sequence (4 bytes) + return before_payload + + +def parse_response(res: bytes): + """Parse server response message. + + Response structure: + - header (size determined by header_size) + - header_extensions (optional, size equals 8 * 4 * (header_size - 1)) + - payload (actual data) + + Returns: + A dictionary containing the parsed results. + """ + header_size = res[0] & 0x0F + message_type = res[1] >> 4 + message_type_specific_flags = res[1] & 0x0F + serialization_method = res[2] >> 4 + message_compression = res[2] & 0x0F + payload = res[header_size * 4 :] # Extract payload section + result = { + "is_last_package": False, # Default to not being the last package + } + payload_msg = None + payload_size = 0 + + # Check if sequence number exists + if message_type_specific_flags & 0x01: + seq = int.from_bytes(payload[:4], "big", signed=True) + result["payload_sequence"] = seq + payload = payload[4:] # Remove sequence number section + + # Check if this is the last package + if message_type_specific_flags & 0x02: + result["is_last_package"] = True + + # Parse payload based on message type + if message_type == FULL_SERVER_RESPONSE: + payload_size = int.from_bytes(payload[:4], "big", signed=True) + payload_msg = payload[4:] + elif message_type == SERVER_ACK: + seq = int.from_bytes(payload[:4], "big", signed=True) + result["seq"] = seq + if len(payload) >= 8: # ACK may contain additional information + payload_size = int.from_bytes(payload[4:8], "big", signed=False) + payload_msg = payload[8:] + elif message_type == SERVER_ERROR_RESPONSE: + code = int.from_bytes(payload[:4], "big", signed=False) + result["code"] = code + payload_size = int.from_bytes(payload[4:8], "big", signed=False) + payload_msg = payload[8:] + + if payload_msg is None: # If no payload message, return directly + return result + + # Handle compression + if message_compression == GZIP: + payload_msg = gzip.decompress(payload_msg) + + # Handle serialization + if serialization_method == JSON: + payload_msg = json.loads(str(payload_msg, "utf-8")) + elif ( + serialization_method != NO_SERIALIZATION + ): # Other non-text serialization, try to convert to string + payload_msg = str(payload_msg, "utf-8") + + result["payload_msg"] = payload_msg + result["payload_size"] = payload_size + return result + + +def convert_audio_to_16k(input_path, output_path=None): + """Convert audio file to 16000Hz MP3 format. + + Args: + input_path: Input audio file path. + output_path: Output audio file path. If None, generates a temp file in the same directory. + + Returns: + Path of the converted audio file. Returns original path if conversion fails or dependencies are missing. + """ + try: + import tempfile + + from pydub import AudioSegment + + # If no output path specified, create temp file + if output_path is None: + temp_dir = str(Path(input_path).parent) + # Use more explicit filename + output_path = tempfile.mktemp( + suffix="_16k.mp3", prefix=Path(input_path).stem + "_", dir=temp_dir + ) + + logger.info(f"Attempting to convert {input_path} to 16kHz MP3...") + audio = AudioSegment.from_file(input_path) + audio = audio.set_frame_rate(16000) + audio.export(output_path, format="mp3") + logger.info(f"Successfully converted audio to 16000Hz: {output_path}") + return output_path + + except ImportError: + logger.warning( + "Missing pydub dependency, cannot convert sample rate. Please run: pip install pydub" + ) + return input_path + except Exception as e: + logger.error(f"Error converting audio sample rate ({input_path}): {e}") + return input_path + + +def convert_audio_to_pcm( + input_path, output_path=None, sample_rate=16000, channels=1, bits_per_sample=16 +): + """Convert audio file to PCM raw format. + + Args: + input_path: Input audio file path. + output_path: Output PCM file path. If None, generates a temp file in the same directory. + sample_rate: Target sample rate (Hz). + channels: Target number of channels. + bits_per_sample: Target bits per sample. + + Returns: + Path of the converted PCM file. Returns original path if conversion fails or dependencies are missing. + """ + try: + import tempfile + + from pydub import AudioSegment + + # If no output path specified, create temp file + if output_path is None: + temp_dir = str(Path(input_path).parent) + # Use more explicit filename + output_path = tempfile.mktemp( + suffix=".pcm", prefix=Path(input_path).stem + "_", dir=temp_dir + ) + + logger.info(f"Attempting to convert {input_path} to PCM...") + audio = AudioSegment.from_file(input_path) + + # Set parameters + audio = audio.set_frame_rate(sample_rate) + audio = audio.set_channels(channels) + audio = audio.set_sample_width( + bits_per_sample // 8 + ) # Convert bit depth to bytes + + # Export as raw PCM + with Path(output_path).open("wb") as f: + f.write(audio.raw_data) + + logger.info(f"Successfully converted audio to PCM format: {output_path}") + logger.info( + f" Parameters: Sample Rate={sample_rate}Hz, Channels={channels}, Bit Depth={bits_per_sample}bit" + ) + return output_path + + except ImportError: + logger.warning( + "Missing pydub dependency, cannot convert to PCM. Please run: pip install pydub" + ) + return input_path + except Exception as e: + logger.error(f"Error converting audio to PCM ({input_path}): {e}") + return input_path + + +def read_wav_info(data: bytes) -> tuple[int, int, int, int, bytes]: + """Read audio information and data from WAV file byte stream. + + Args: + data: WAV file byte content. + + Returns: + A tuple containing: (channels, sample width (bytes), frame rate, frame count, audio data). + """ + with BytesIO(data) as _f, wave.open(_f, "rb") as wave_fp: + nchannels, sampwidth, framerate, nframes = wave_fp.getparams()[:4] + wave_bytes = wave_fp.readframes(nframes) + return nchannels, sampwidth, framerate, nframes, wave_bytes + + +class AsrWsClient: + """Client for communicating with ASR service via WebSocket.""" + + def __init__(self, audio_path: str, **kwargs): + """Initialize ASR WebSocket client. + + Args: + audio_path: Path to the audio file for recognition. + **kwargs: Other configuration parameters, such as: + seg_duration (int): Audio segment duration (ms), default 100. + ws_url (str): WebSocket service address. + uid (str): User ID. + format (str): Audio format (e.g., "mp3", "wav", "pcm"). + rate (int): Sample rate (Hz). + bits (int): Bit depth. + channel (int): Number of channels. + codec (str): Encoding format (e.g., "raw"). + streaming (bool): Enable simulated delay for streaming, default True. + mp3_seg_size (int): MP3 format segment size (bytes), default 1000. + api_resource_id (str): API resource ID. + api_access_key (str): API access key. + api_app_key (str): API application key. + """ + self.audio_path = audio_path + self.seg_duration = int(kwargs.get("seg_duration", 100)) + self.ws_url = kwargs.get( + "ws_url", "wss://openspeech.bytedance.com/api/v3/sauc/bigmodel" + ) + self.uid = kwargs.get("uid", "test_user") + self.format = kwargs.get("format", "mp3") + self.rate = kwargs.get("rate", 16000) + self.bits = kwargs.get("bits", 16) + self.channel = kwargs.get("channel", 1) + self.codec = kwargs.get("codec", "raw") + self.streaming = kwargs.get("streaming", True) + self.mp3_seg_size = kwargs.get("mp3_seg_size", 1000) + # API authentication information + self.api_resource_id = kwargs.get( + "api_resource_id", "volc.bigasr.sauc.duration" + ) + self.api_access_key = kwargs.get("api_access_key", "") + self.api_app_key = kwargs.get("api_app_key", "") + + def construct_request(self, reqid: str) -> dict: + """Build initial request parameter dictionary.""" + req = { + "user": { + "uid": self.uid, + }, + "audio": { + "format": self.format, + "sample_rate": self.rate, + "bits": self.bits, + "channel": self.channel, + "codec": self.codec, + }, + "request": { + "model_name": "bigmodel", + "enable_punc": True, + }, + } + return req + + @staticmethod + def slice_data( + data: bytes, chunk_size: int + ) -> Generator[tuple[bytes, bool], None, None]: + """Slice byte data into specified size chunks. + + Yields: + A tuple (chunk, is_last), where chunk is the data block and is_last indicates if it's the final block. + """ + data_len = len(data) + offset = 0 + while offset + chunk_size < data_len: + yield data[offset : offset + chunk_size], False + offset += chunk_size + else: + yield data[offset:data_len], True + + async def segment_data_processor(self, wav_data: bytes, segment_size: int): + """Process audio data segments and communicate with WebSocket server.""" + reqid = str(uuid.uuid4()) + seq = 1 + request_params = self.construct_request(reqid) + + # Prepare initial request (FULL_CLIENT_REQUEST) + payload_bytes = str.encode(json.dumps(request_params)) + payload_bytes = gzip.compress(payload_bytes) + full_client_request = bytearray( + generate_header(message_type_specific_flags=POS_SEQUENCE) + ) + full_client_request.extend(generate_before_payload(sequence=seq)) + full_client_request.extend((len(payload_bytes)).to_bytes(4, "big")) + full_client_request.extend(payload_bytes) + + # Prepare WebSocket request headers + header = { + "X-Api-Resource-Id": self.api_resource_id, + "X-Api-Access-Key": self.api_access_key, + "X-Api-App-Key": self.api_app_key, + "X-Api-Request-Id": reqid, + } + + logger.info(f"[{reqid}] Starting WebSocket connection: {self.ws_url}") + try: + async with websockets.connect( + self.ws_url, + extra_headers=header, + max_size=1000000000, + open_timeout=30, # Increase connection timeout + ) as ws: + logger.info( + f"[{reqid}] WebSocket connected successfully, sending initial request..." + ) + await ws.send(full_client_request) + res = await ws.recv() + result = parse_response(res) + logger.info( + f"[{reqid}] Received initial response parse result: {result}" + ) + + logger.info(f"[{reqid}] Starting to send audio data stream...") + # Iterate through audio data chunks + for _, (chunk, last) in enumerate( + AsrWsClient.slice_data(wav_data, chunk_size=segment_size), 1 + ): + seq += 1 + current_seq = seq if not last else -seq + start_time = time.time() + + payload_bytes = gzip.compress(chunk) + + flags = NEG_WITH_SEQUENCE if last else POS_SEQUENCE + audio_only_request = bytearray( + generate_header( + message_type=AUDIO_ONLY_REQUEST, + message_type_specific_flags=flags, + ) + ) + audio_only_request.extend( + generate_before_payload(sequence=current_seq) + ) + audio_only_request.extend((len(payload_bytes)).to_bytes(4, "big")) + audio_only_request.extend(payload_bytes) + + await ws.send(audio_only_request) + + res = await ws.recv() + result = parse_response(res) + logger.info(f"[{reqid}] Seq {current_seq} response: {result}") + + if self.streaming: + elapsed_time = time.time() - start_time + sleep_time = max(0, (self.seg_duration / 1000.0 - elapsed_time)) + if sleep_time > 0: + await asyncio.sleep(sleep_time) + + logger.info(f"[{reqid}] All audio data sent successfully.") + # Theoretically, the last recv should contain the final recognition result or confirmation + # result variable contains the last received parsed response + return result # Return the last received result + except websockets.exceptions.ConnectionClosedError as e: + logger.warning( + f"[{reqid}] WebSocket connection closed: Code={e.code}, Reason='{e.reason}'" + ) + return {"error": "ConnectionClosed", "code": e.code, "reason": e.reason} + except websockets.exceptions.WebSocketException as e: + logger.error(f"[{reqid}] WebSocket connection failed: {e}") + error_details = {"error": "WebSocketException", "message": str(e)} + if hasattr(e, "status_code"): + error_details["status_code"] = e.status_code + if hasattr(e, "headers"): + error_details["headers"] = dict(e.headers) + return error_details + except Exception as e: + logger.error( + f"[{reqid}] Unexpected error during processing: {e}", exc_info=True + ) + return {"error": "UnexpectedError", "message": str(e)} + + async def execute(self) -> dict: + """Execute ASR task: Read audio file and invoke segment processor.""" + logger.info(f"Start processing audio file: {self.audio_path}") + original_path = self.audio_path + audio_path = original_path + temp_file_to_clean = None + + try: + # Check file format and perform necessary conversions + if not Path(original_path).exists(): + raise FileNotFoundError(f"Audio file not found: {original_path}") + + # Step 1: Ensure audio sample rate is 16kHz + temp_16k_path = None + if self.format.lower() == "mp3": + logger.info(f"Converting MP3 file to 16kHz: {original_path}") + temp_16k_path = convert_audio_to_16k(original_path) + if temp_16k_path == original_path: + raise ValueError("Failed to convert MP3 file to 16kHz") + logger.info(f"MP3 file converted to 16kHz: {temp_16k_path}") + else: + temp_16k_path = original_path + + # Step 2: Convert all audio to PCM format + logger.info(f"Converting audio to PCM format: {temp_16k_path}") + converted_path = convert_audio_to_pcm( + temp_16k_path, + sample_rate=16000, # Force 16kHz sample rate + channels=self.channel, + bits_per_sample=self.bits, + ) + if converted_path == temp_16k_path: + if temp_16k_path and temp_16k_path != original_path: + # Clean up 16kHz temp file + try: + Path(temp_16k_path).unlink() + except OSError as e: + logger.warning( + f"Failed to clean up temp file: {temp_16k_path}, Error: {e}" + ) + raise ValueError("Failed to convert audio to PCM format") + + # Clean up 16kHz temp file if it exists and is not the original file + if temp_16k_path and temp_16k_path != original_path: + try: + Path(temp_16k_path).unlink() + logger.info(f"Cleaned up intermediate temp file: {temp_16k_path}") + except OSError as e: + logger.warning( + f"Failed to clean up temp file: {temp_16k_path}, Error: {e}" + ) + + audio_path = converted_path + temp_file_to_clean = converted_path + self.rate = 16000 # Update sample rate parameter + self.format = "pcm" # Update format to pcm + logger.info(f"Audio converted to 16kHz PCM format: {audio_path}") + + # Read converted audio data + try: + async with aiofiles.open(audio_path, mode="rb") as _f: + audio_data = await _f.read() + logger.info( + f"Successfully read audio file {audio_path}, size: {len(audio_data)} bytes" + ) + + if len(audio_data) == 0: + raise ValueError("Audio file is empty") + except Exception as e: + raise OSError(f"Failed to read audio file: {e}") from e + + segment_size = 0 + if self.format == "mp3": + segment_size = self.mp3_seg_size + logger.info(f"Using MP3 segment size: {segment_size} bytes") + elif self.format == "wav": + try: + nchannels, sampwidth, framerate, _, _ = read_wav_info(audio_data) + if ( + framerate != self.rate + or nchannels != self.channel + or sampwidth * 8 != self.bits + ): + logger.warning( + f"WAV file parameters ({framerate}Hz, {nchannels}ch, {sampwidth * 8}bit) " + f"don't match configuration ({self.rate}Hz, {self.channel}ch, {self.bits}bit). " + "Results may be inaccurate." + ) + size_per_sec = nchannels * sampwidth * framerate + segment_size = int(size_per_sec * self.seg_duration / 1000) + logger.info( + f"Calculated WAV segment size: {segment_size} bytes (based on {self.seg_duration}ms)" + ) + except Exception as e: + logger.error(f"Failed to read WAV file info: {e}") + raise ValueError("Unable to read WAV file information") from e + elif self.format == "pcm": + bytes_per_sample = self.bits // 8 + segment_size = int( + self.rate + * bytes_per_sample + * self.channel + * self.seg_duration + / 1000 + ) + logger.info( + f"Calculated PCM segment size: {segment_size} bytes (based on {self.seg_duration}ms)" + ) + else: + logger.error(f"Unsupported audio format: {self.format}") + raise ValueError(f"Unsupported audio format: {self.format}") + + if segment_size <= 0: + logger.error( + "Invalid calculated segment size, please check parameters." + ) + raise ValueError("Invalid calculated segment size") + + result = await self.segment_data_processor(audio_data, segment_size) + + # Clean up temporary files after processing + if temp_file_to_clean and Path(temp_file_to_clean).exists(): + try: + Path(temp_file_to_clean).unlink() + logger.info(f"Cleaned up temp file: {temp_file_to_clean}") + except OSError as e: + logger.warning( + f"Failed to clean up temp file: {temp_file_to_clean}, Error: {e}" + ) + + return result + + except FileNotFoundError: + logger.error(f"Audio file not found: {self.audio_path}") + return {"error": "FileNotFound", "path": self.audio_path} + except Exception as e: + logger.error(f"Error executing ASR task: {e}", exc_info=True) + # Clean up temporary files even when error occurs + if temp_file_to_clean and Path(temp_file_to_clean).exists(): + try: + Path(temp_file_to_clean).unlink() + logger.info(f"Cleaning up temp file: {temp_file_to_clean}") + except OSError as ex: + logger.warning( + f"Failed to clean up temp file: {temp_file_to_clean}, Error: {ex}" + ) + return {"error": "ExecutionError", "message": str(e)} diff --git a/mcp/server/mcp_server_speech/src/mcp_server_speech/services/tts.py b/mcp/server/mcp_server_speech/src/mcp_server_speech/services/tts.py new file mode 100644 index 00000000..01859c76 --- /dev/null +++ b/mcp/server/mcp_server_speech/src/mcp_server_speech/services/tts.py @@ -0,0 +1,157 @@ +import asyncio +import base64 +import json +import logging +import time +import uuid # For generating unique request IDs +from pathlib import Path + +import requests # Use requests library + +from mcp_server_speech.config import ( + VOLC_APPID, + VOLC_CLUSTER, + VOLC_TOKEN, + VOLC_VOICE_TYPE, +) +from mcp_server_speech.models import TtsInputArgs, TtsOutputResult + +logger = logging.getLogger(__name__) + +TTS_API_URL = "https://openspeech.bytedance.com/api/v1/tts" + + +def _make_sync_tts_request(reqid: str, args: TtsInputArgs) -> tuple[str | None, str]: + """Synchronous function to make the TTS request using requests.""" + payload = { + "app": { + "appid": VOLC_APPID, + "token": VOLC_TOKEN, + "cluster": VOLC_CLUSTER, + }, + "user": {"uid": "mcp_server_user"}, # Generic user ID for server-side requests + "audio": { + "voice_type": VOLC_VOICE_TYPE, + "encoding": args.encoding, + "speed_ratio": args.speed, + }, + "request": { + "reqid": reqid, + "text": args.text, + "operation": "query", + }, + } + headers = { + "Authorization": f"Bearer;{VOLC_TOKEN}", + "Content-Type": "application/json", + } + + try: + response = requests.post( + TTS_API_URL, headers=headers, json=payload, timeout=30 + ) # Add timeout + response.raise_for_status() # Raise HTTPError for bad responses (4xx or 5xx) + + json_data = response.json() + logger.info(f"TTS API response: {json_data}") + + # Check for API-level errors + if json_data.get("code") != 3000: + error_msg = f"TTS API returned an error: Code {json_data.get('code')}, Message: {json_data.get('message')}" + logger.error(error_msg) + raise ValueError(error_msg) + + audio_base64 = json_data.get("data") + if not audio_base64: + logger.error("TTS API response did not contain audio data.") + raise ValueError("No audio data received from TTS API") + + return audio_base64, args.encoding + + except requests.exceptions.Timeout as err: + logger.error(f"TTS API request timed out for reqid: {reqid}") + raise TimeoutError("TTS API request timed out") from err + except requests.exceptions.HTTPError as e: + logger.error( + f"TTS API HTTP error for reqid {reqid}: {e.response.status_code} {e.response.reason} - Response: {e.response.text}" + ) + raise # Re-raise the HTTPError + except requests.exceptions.RequestException as e: + logger.exception(f"Error during TTS request for reqid {reqid}: {e}") + raise # Re-raise other request exceptions + except json.JSONDecodeError as e: + logger.exception( + f"Error decoding TTS API response for reqid {reqid}: {e} - Response: {response.text}" + ) + raise ValueError("Invalid response format from TTS API") from e + except Exception as e: + logger.exception( + f"An unexpected error occurred during synchronous TTS request for reqid {reqid}: {e}" + ) + raise + + +# Keep the core logic separate +async def tts_request_handler(args: TtsInputArgs) -> TtsOutputResult: + """ + Synthesizes text into speech using Bytedance API (via requests) and returns Base64 encoded audio data. + Runs the synchronous requests call in a separate thread. + """ + logger.info( + f"Received TTS request for text: '{args.text[:50]}...' (format: {args.encoding})" # Log truncated text, removed unused language + ) + + if not all([VOLC_APPID, VOLC_TOKEN]): + raise ValueError("VOLC_APPID or VOLC_TOKEN is not configured.") + + reqid = str(uuid.uuid4()) + + try: + # Run the synchronous function in a thread pool executor + audio_base64, output_format = await asyncio.to_thread( + _make_sync_tts_request, reqid, args + ) + + # Decode the base64 audio data to mp3 and save it to a temporary file + temp_dir = Path("temp") + temp_dir.mkdir(parents=True, exist_ok=True) + temp_file_path = temp_dir / f"{int(time.time())}.mp3" + with temp_file_path.open("wb") as temp_file: + temp_file.write(base64.b64decode(audio_base64)) + + logger.info( + f"TTS successful: Request ID {reqid}, file path {temp_file_path.absolute()}, format: {output_format}" + ) + return TtsOutputResult( + file_path=Path(temp_file_path).absolute().as_posix(), format=output_format + ) + + except (ValueError, TimeoutError, requests.exceptions.RequestException) as e: + # Log specific errors already handled in the sync function or asyncio wrapper + logger.error(f"TTS request failed for reqid {reqid}: {e}") + # Re-raise the specific error for potential handling upstream + raise + except Exception as e: + # Catch any other unexpected errors during the async execution + logger.exception( + f"An unexpected error occurred during async TTS processing for reqid {reqid}: {e}" + ) + raise # Re-raise generic exceptions + + +if __name__ == "__main__": + from mcp_server_speech.config import load_config + + # test _tts_logic + def test_tts_logic(): + load_config() + + args = TtsInputArgs( + text="Hello, this is a test.", + speed=1.0, + encoding="mp3", + ) + result = asyncio.run(tts_request_handler(args)) + logger.info(f"Test result: {result}") + + test_tts_logic() diff --git a/mcp/server/mcp_server_speech/uv.lock b/mcp/server/mcp_server_speech/uv.lock new file mode 100644 index 00000000..c2dc11c6 --- /dev/null +++ b/mcp/server/mcp_server_speech/uv.lock @@ -0,0 +1,481 @@ +version = 1 +revision = 1 +requires-python = ">=3.12" + +[[package]] +name = "aiofiles" +version = "24.1.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/0b/03/a88171e277e8caa88a4c77808c20ebb04ba74cc4681bf1e9416c862de237/aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53" }, +] + +[[package]] +name = "anyio" +version = "4.9.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c" }, +] + +[[package]] +name = "certifi" +version = "2025.1.31" +source = { registry = "https://mirrors.aliyun.com/pypi/simple" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.1" +source = { registry = "https://mirrors.aliyun.com/pypi/simple" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7" }, + { url = "https://mirrors.aliyun.com/pypi/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757" }, + { url = "https://mirrors.aliyun.com/pypi/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa" }, + { url = "https://mirrors.aliyun.com/pypi/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d" }, + { url = "https://mirrors.aliyun.com/pypi/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616" }, + { url = "https://mirrors.aliyun.com/pypi/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b" }, + { url = "https://mirrors.aliyun.com/pypi/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d" }, + { url = "https://mirrors.aliyun.com/pypi/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a" }, + { url = "https://mirrors.aliyun.com/pypi/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9" }, + { url = "https://mirrors.aliyun.com/pypi/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1" }, + { url = "https://mirrors.aliyun.com/pypi/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35" }, + { url = "https://mirrors.aliyun.com/pypi/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f" }, + { url = "https://mirrors.aliyun.com/pypi/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda" }, + { url = "https://mirrors.aliyun.com/pypi/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313" }, + { url = "https://mirrors.aliyun.com/pypi/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9" }, + { url = "https://mirrors.aliyun.com/pypi/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b" }, + { url = "https://mirrors.aliyun.com/pypi/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11" }, + { url = "https://mirrors.aliyun.com/pypi/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f" }, + { url = "https://mirrors.aliyun.com/pypi/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd" }, + { url = "https://mirrors.aliyun.com/pypi/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2" }, + { url = "https://mirrors.aliyun.com/pypi/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886" }, + { url = "https://mirrors.aliyun.com/pypi/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601" }, + { url = "https://mirrors.aliyun.com/pypi/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd" }, + { url = "https://mirrors.aliyun.com/pypi/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407" }, + { url = "https://mirrors.aliyun.com/pypi/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971" }, + { url = "https://mirrors.aliyun.com/pypi/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85" }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://mirrors.aliyun.com/pypi/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://mirrors.aliyun.com/pypi/simple" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" }, +] + +[[package]] +name = "h11" +version = "0.14.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761" }, +] + +[[package]] +name = "httpcore" +version = "1.0.8" +source = { registry = "https://mirrors.aliyun.com/pypi/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/9f/45/ad3e1b4d448f22c0cff4f5692f5ed0666658578e358b8d58a19846048059/httpcore-1.0.8.tar.gz", hash = "sha256:86e94505ed24ea06514883fd44d2bc02d90e77e7979c8eb71b90f41d364a1bad" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/18/8d/f052b1e336bb2c1fc7ed1aaed898aa570c0b61a09707b108979d9fc6e308/httpcore-1.0.8-py3-none-any.whl", hash = "sha256:5254cf149bcb5f75e9d1b2b9f729ea4a4b883d1ad7379fc632b727cec23674be" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://mirrors.aliyun.com/pypi/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://mirrors.aliyun.com/pypi/simple" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1" }, +] + +[[package]] +name = "mcp" +version = "1.6.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "uvicorn" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/95/d2/f587cb965a56e992634bebc8611c5b579af912b74e04eb9164bd49527d21/mcp-1.6.0.tar.gz", hash = "sha256:d9324876de2c5637369f43161cd71eebfd803df5a95e46225cab8d280e366723" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/10/30/20a7f33b0b884a9d14dd3aa94ff1ac9da1479fe2ad66dd9e2736075d2506/mcp-1.6.0-py3-none-any.whl", hash = "sha256:7bd24c6ea042dbec44c754f100984d186620d8b841ec30f1b19eda9b93a634d0" }, +] + +[package.optional-dependencies] +cli = [ + { name = "python-dotenv" }, + { name = "typer" }, +] + +[[package]] +name = "mcp-server-speech" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "aiofiles" }, + { name = "mcp", extra = ["cli"] }, + { name = "pydub" }, + { name = "python-dotenv" }, + { name = "requests" }, + { name = "websockets" }, +] + +[package.metadata] +requires-dist = [ + { name = "aiofiles", specifier = ">=24.1.0" }, + { name = "mcp", extras = ["cli"], specifier = ">=1.6.0" }, + { name = "pydub", specifier = ">=0.25.1" }, + { name = "python-dotenv", specifier = ">=1.1.0" }, + { name = "requests", specifier = ">=2.32.3" }, + { name = "websockets", specifier = "<14" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://mirrors.aliyun.com/pypi/simple" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8" }, +] + +[[package]] +name = "pydantic" +version = "2.11.3" +source = { registry = "https://mirrors.aliyun.com/pypi/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/10/2e/ca897f093ee6c5f3b0bee123ee4465c50e75431c3d5b6a3b44a47134e891/pydantic-2.11.3.tar.gz", hash = "sha256:7471657138c16adad9322fe3070c0116dd6c3ad8d649300e3cbdfe91f4db4ec3" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/b0/1d/407b29780a289868ed696d1616f4aad49d6388e5a77f567dcd2629dcd7b8/pydantic-2.11.3-py3-none-any.whl", hash = "sha256:a082753436a07f9ba1289c6ffa01cd93db3548776088aa917cc43b63f68fa60f" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.1" +source = { registry = "https://mirrors.aliyun.com/pypi/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/17/19/ed6a078a5287aea7922de6841ef4c06157931622c89c2a47940837b5eecd/pydantic_core-2.33.1.tar.gz", hash = "sha256:bcc9c6fdb0ced789245b02b7d6603e17d1563064ddcfc36f046b61c0c05dd9df" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/c8/ce/3cb22b07c29938f97ff5f5bb27521f95e2ebec399b882392deb68d6c440e/pydantic_core-2.33.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1293d7febb995e9d3ec3ea09caf1a26214eec45b0f29f6074abb004723fc1de8" }, + { url = "https://mirrors.aliyun.com/pypi/packages/19/78/f381d643b12378fee782a72126ec5d793081ef03791c28a0fd542a5bee64/pydantic_core-2.33.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:99b56acd433386c8f20be5c4000786d1e7ca0523c8eefc995d14d79c7a081498" }, + { url = "https://mirrors.aliyun.com/pypi/packages/9d/2b/98a37b80b15aac9eb2c6cfc6dbd35e5058a352891c5cce3a8472d77665a6/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35a5ec3fa8c2fe6c53e1b2ccc2454398f95d5393ab398478f53e1afbbeb4d939" }, + { url = "https://mirrors.aliyun.com/pypi/packages/4e/d4/3c59514e0f55a161004792b9ff3039da52448f43f5834f905abef9db6e4a/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b172f7b9d2f3abc0efd12e3386f7e48b576ef309544ac3a63e5e9cdd2e24585d" }, + { url = "https://mirrors.aliyun.com/pypi/packages/a9/b6/c2c7946ef70576f79a25db59a576bce088bdc5952d1b93c9789b091df716/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9097b9f17f91eea659b9ec58148c0747ec354a42f7389b9d50701610d86f812e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/88/fe/65a880f81e3f2a974312b61f82a03d85528f89a010ce21ad92f109d94deb/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc77ec5b7e2118b152b0d886c7514a4653bcb58c6b1d760134a9fab915f777b3" }, + { url = "https://mirrors.aliyun.com/pypi/packages/6f/ff/4459e4146afd0462fb483bb98aa2436d69c484737feaceba1341615fb0ac/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3d15245b08fa4a84cefc6c9222e6f37c98111c8679fbd94aa145f9a0ae23d" }, + { url = "https://mirrors.aliyun.com/pypi/packages/7c/76/1c42e384e8d78452ededac8b583fe2550c84abfef83a0552e0e7478ccbc3/pydantic_core-2.33.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef99779001d7ac2e2461d8ab55d3373fe7315caefdbecd8ced75304ae5a6fc6b" }, + { url = "https://mirrors.aliyun.com/pypi/packages/00/72/7d0cf05095c15f7ffe0eb78914b166d591c0eed72f294da68378da205101/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fc6bf8869e193855e8d91d91f6bf59699a5cdfaa47a404e278e776dd7f168b39" }, + { url = "https://mirrors.aliyun.com/pypi/packages/b3/69/94a514066bb7d8be499aa764926937409d2389c09be0b5107a970286ef81/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:b1caa0bc2741b043db7823843e1bde8aaa58a55a58fda06083b0569f8b45693a" }, + { url = "https://mirrors.aliyun.com/pypi/packages/84/b0/e390071eadb44b41f4f54c3cef64d8bf5f9612c92686c9299eaa09e267e2/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ec259f62538e8bf364903a7d0d0239447059f9434b284f5536e8402b7dd198db" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d6/b2/288b3579ffc07e92af66e2f1a11be3b056fe1214aab314748461f21a31c3/pydantic_core-2.33.1-cp312-cp312-win32.whl", hash = "sha256:e14f369c98a7c15772b9da98987f58e2b509a93235582838bd0d1d8c08b68fda" }, + { url = "https://mirrors.aliyun.com/pypi/packages/02/28/58442ad1c22b5b6742b992ba9518420235adced665513868f99a1c2638a5/pydantic_core-2.33.1-cp312-cp312-win_amd64.whl", hash = "sha256:1c607801d85e2e123357b3893f82c97a42856192997b95b4d8325deb1cd0c5f4" }, + { url = "https://mirrors.aliyun.com/pypi/packages/a1/eb/f54809b51c7e2a1d9f439f158b8dd94359321abcc98767e16fc48ae5a77e/pydantic_core-2.33.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d13f0276806ee722e70a1c93da19748594f19ac4299c7e41237fc791d1861ea" }, + { url = "https://mirrors.aliyun.com/pypi/packages/7a/24/eed3466a4308d79155f1cdd5c7432c80ddcc4530ba8623b79d5ced021641/pydantic_core-2.33.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:70af6a21237b53d1fe7b9325b20e65cbf2f0a848cf77bed492b029139701e66a" }, + { url = "https://mirrors.aliyun.com/pypi/packages/ab/14/df54b1a0bc9b6ded9b758b73139d2c11b4e8eb43e8ab9c5847c0a2913ada/pydantic_core-2.33.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:282b3fe1bbbe5ae35224a0dbd05aed9ccabccd241e8e6b60370484234b456266" }, + { url = "https://mirrors.aliyun.com/pypi/packages/fa/96/e275f15ff3d34bb04b0125d9bc8848bf69f25d784d92a63676112451bfb9/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b315e596282bbb5822d0c7ee9d255595bd7506d1cb20c2911a4da0b970187d3" }, + { url = "https://mirrors.aliyun.com/pypi/packages/b7/d8/96bc536e975b69e3a924b507d2a19aedbf50b24e08c80fb00e35f9baaed8/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1dfae24cf9921875ca0ca6a8ecb4bb2f13c855794ed0d468d6abbec6e6dcd44a" }, + { url = "https://mirrors.aliyun.com/pypi/packages/90/72/ab58e43ce7e900b88cb571ed057b2fcd0e95b708a2e0bed475b10130393e/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6dd8ecfde08d8bfadaea669e83c63939af76f4cf5538a72597016edfa3fad516" }, + { url = "https://mirrors.aliyun.com/pypi/packages/dc/3f/52d85781406886c6870ac995ec0ba7ccc028b530b0798c9080531b409fdb/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f593494876eae852dc98c43c6f260f45abdbfeec9e4324e31a481d948214764" }, + { url = "https://mirrors.aliyun.com/pypi/packages/f4/56/6e2ef42f363a0eec0fd92f74a91e0ac48cd2e49b695aac1509ad81eee86a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:948b73114f47fd7016088e5186d13faf5e1b2fe83f5e320e371f035557fd264d" }, + { url = "https://mirrors.aliyun.com/pypi/packages/4c/c0/604536c4379cc78359f9ee0aa319f4aedf6b652ec2854953f5a14fc38c5a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e11f3864eb516af21b01e25fac915a82e9ddad3bb0fb9e95a246067398b435a4" }, + { url = "https://mirrors.aliyun.com/pypi/packages/1f/46/9eb764814f508f0edfb291a0f75d10854d78113fa13900ce13729aaec3ae/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:549150be302428b56fdad0c23c2741dcdb5572413776826c965619a25d9c6bde" }, + { url = "https://mirrors.aliyun.com/pypi/packages/42/e3/fb6b2a732b82d1666fa6bf53e3627867ea3131c5f39f98ce92141e3e3dc1/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:495bc156026efafd9ef2d82372bd38afce78ddd82bf28ef5276c469e57c0c83e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/5c/9d/fbe8fe9d1aa4dac88723f10a921bc7418bd3378a567cb5e21193a3c48b43/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ec79de2a8680b1a67a07490bddf9636d5c2fab609ba8c57597e855fa5fa4dacd" }, + { url = "https://mirrors.aliyun.com/pypi/packages/aa/99/07e2237b8a66438d9b26482332cda99a9acccb58d284af7bc7c946a42fd3/pydantic_core-2.33.1-cp313-cp313-win32.whl", hash = "sha256:ee12a7be1742f81b8a65b36c6921022301d466b82d80315d215c4c691724986f" }, + { url = "https://mirrors.aliyun.com/pypi/packages/8a/f4/e457a7849beeed1e5defbcf5051c6f7b3c91a0624dd31543a64fc9adcf52/pydantic_core-2.33.1-cp313-cp313-win_amd64.whl", hash = "sha256:ede9b407e39949d2afc46385ce6bd6e11588660c26f80576c11c958e6647bc40" }, + { url = "https://mirrors.aliyun.com/pypi/packages/20/d0/e8d567a7cff7b04e017ae164d98011f1e1894269fe8e90ea187a3cbfb562/pydantic_core-2.33.1-cp313-cp313-win_arm64.whl", hash = "sha256:aa687a23d4b7871a00e03ca96a09cad0f28f443690d300500603bd0adba4b523" }, + { url = "https://mirrors.aliyun.com/pypi/packages/ef/fd/24ea4302d7a527d672c5be06e17df16aabfb4e9fdc6e0b345c21580f3d2a/pydantic_core-2.33.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:401d7b76e1000d0dd5538e6381d28febdcacb097c8d340dde7d7fc6e13e9f95d" }, + { url = "https://mirrors.aliyun.com/pypi/packages/5f/95/4fbc2ecdeb5c1c53f1175a32d870250194eb2fdf6291b795ab08c8646d5d/pydantic_core-2.33.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7aeb055a42d734c0255c9e489ac67e75397d59c6fbe60d155851e9782f276a9c" }, + { url = "https://mirrors.aliyun.com/pypi/packages/71/ae/fe31e7f4a62431222d8f65a3bd02e3fa7e6026d154a00818e6d30520ea77/pydantic_core-2.33.1-cp313-cp313t-win_amd64.whl", hash = "sha256:338ea9b73e6e109f15ab439e62cb3b78aa752c7fd9536794112e14bee02c8d18" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.8.1" +source = { registry = "https://mirrors.aliyun.com/pypi/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/88/82/c79424d7d8c29b994fb01d277da57b0a9b09cc03c3ff875f9bd8a86b2145/pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/0b/53/a64f03044927dc47aafe029c42a5b7aabc38dfb813475e0e1bf71c4a59d0/pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c" }, +] + +[[package]] +name = "pydub" +version = "0.25.1" +source = { registry = "https://mirrors.aliyun.com/pypi/simple" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/fe/9a/e6bca0eed82db26562c73b5076539a4a08d3cffd19c3cc5913a3e61145fd/pydub-0.25.1.tar.gz", hash = "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/a6/53/d78dc063216e62fc55f6b2eebb447f6a4b0a59f55c8406376f76bf959b08/pydub-0.25.1-py2.py3-none-any.whl", hash = "sha256:65617e33033874b59d87db603aa1ed450633288aefead953b30bded59cb599a6" }, +] + +[[package]] +name = "pygments" +version = "2.19.1" +source = { registry = "https://mirrors.aliyun.com/pypi/simple" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c" }, +] + +[[package]] +name = "python-dotenv" +version = "1.1.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d" }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://mirrors.aliyun.com/pypi/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" }, +] + +[[package]] +name = "rich" +version = "14.0.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://mirrors.aliyun.com/pypi/simple" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://mirrors.aliyun.com/pypi/simple" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2" }, +] + +[[package]] +name = "sse-starlette" +version = "2.2.1" +source = { registry = "https://mirrors.aliyun.com/pypi/simple" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/71/a4/80d2a11af59fe75b48230846989e93979c892d3a20016b42bb44edb9e398/sse_starlette-2.2.1.tar.gz", hash = "sha256:54470d5f19274aeed6b2d473430b08b4b379ea851d953b11d7f1c4a2c118b419" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/d9/e0/5b8bd393f27f4a62461c5cf2479c75a2cc2ffa330976f9f00f5f6e4f50eb/sse_starlette-2.2.1-py3-none-any.whl", hash = "sha256:6410a3d3ba0c89e7675d4c273a301d64649c03a5ef1ca101f10b47f895fd0e99" }, +] + +[[package]] +name = "starlette" +version = "0.46.2" +source = { registry = "https://mirrors.aliyun.com/pypi/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35" }, +] + +[[package]] +name = "typer" +version = "0.15.2" +source = { registry = "https://mirrors.aliyun.com/pypi/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/8b/6f/3991f0f1c7fcb2df31aef28e0594d8d54b05393a0e4e34c65e475c2a5d41/typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/7f/fc/5b29fea8cee020515ca82cc68e3b8e1e34bb19a3535ad854cac9257b414c/typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc" }, +] + +[[package]] +name = "typing-extensions" +version = "4.13.2" +source = { registry = "https://mirrors.aliyun.com/pypi/simple" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f" }, +] + +[[package]] +name = "urllib3" +version = "2.4.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813" }, +] + +[[package]] +name = "uvicorn" +version = "0.34.1" +source = { registry = "https://mirrors.aliyun.com/pypi/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/86/37/dd92f1f9cedb5eaf74d9999044306e06abe65344ff197864175dbbd91871/uvicorn-0.34.1.tar.gz", hash = "sha256:af981725fc4b7ffc5cb3b0e9eda6258a90c4b52cb2a83ce567ae0a7ae1757afc" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/5f/38/a5801450940a858c102a7ad9e6150146a25406a119851c993148d56ab041/uvicorn-0.34.1-py3-none-any.whl", hash = "sha256:984c3a8c7ca18ebaad15995ee7401179212c59521e67bfc390c07fa2b8d2e065" }, +] + +[[package]] +name = "websockets" +version = "13.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/0f/b0/e53bdd53d86447d211694f3cf66f163d077c5d68e6bcaa726bf64e88ae3a/websockets-13.0.tar.gz", hash = "sha256:b7bf950234a482b7461afdb2ec99eee3548ec4d53f418c7990bb79c620476602" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/ad/0a/baeea2931827e73ebe3d958fad9df74ec66d08341d0cf701ced0381adc91/websockets-13.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b5407c34776b9b77bd89a5f95eb0a34aaf91889e3f911c63f13035220eb50107" }, + { url = "https://mirrors.aliyun.com/pypi/packages/6d/f7/306e2940829db34c5866e869eb5b1a08dd04d1c6d25c71327a028d124871/websockets-13.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4782ec789f059f888c1e8fdf94383d0e64b531cffebbf26dd55afd53ab487ca4" }, + { url = "https://mirrors.aliyun.com/pypi/packages/2b/3c/183a4f79e0ce6be8733f824e0a48db3771a373a7206aef900bc1ae4c176e/websockets-13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c8feb8e19ef65c9994e652c5b0324abd657bedd0abeb946fb4f5163012c1e730" }, + { url = "https://mirrors.aliyun.com/pypi/packages/03/32/37e1c9dd9aa1e7fa6fb3147d6992d61a20ba63ffee2adc88a392e1ae7376/websockets-13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f3d2e20c442b58dbac593cb1e02bc02d149a86056cc4126d977ad902472e3b" }, + { url = "https://mirrors.aliyun.com/pypi/packages/6c/da/0cace6358289c7de1ee02ed0d572dfe92e5cb97270bda60f04a4e49ac5c5/websockets-13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e39d393e0ab5b8bd01717cc26f2922026050188947ff54fe6a49dc489f7750b7" }, + { url = "https://mirrors.aliyun.com/pypi/packages/c7/ab/b763b0e8598c4251ec6e17d18f46cbced157772b991200fb0d32550844c5/websockets-13.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f661a4205741bdc88ac9c2b2ec003c72cee97e4acd156eb733662ff004ba429" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d0/2d/40b8c3ba08792c2ecdb81613671a4b9bd33b83c50519b235e8eeb0ae21a0/websockets-13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:384129ad0490e06bab2b98c1da9b488acb35bb11e2464c728376c6f55f0d45f3" }, + { url = "https://mirrors.aliyun.com/pypi/packages/4c/5e/9a42db20f6c38d247a900bfb8633953df93d8873a99ed9432645a4d5e185/websockets-13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:df5c0eff91f61b8205a6c9f7b255ff390cdb77b61c7b41f79ca10afcbb22b6cb" }, + { url = "https://mirrors.aliyun.com/pypi/packages/87/52/7fb5f052eefaa5d2b42da06b314c2af0467fadbd7f360716a1a4d4f7ab67/websockets-13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:02cc9bb1a887dac0e08bf657c5d00aa3fac0d03215d35a599130c2034ae6663a" }, + { url = "https://mirrors.aliyun.com/pypi/packages/9c/8b/4b7064d1a40fcb85f64bc051d8bdc8a9e388572eb5bec5cb85ffb2c43e01/websockets-13.0-cp312-cp312-win32.whl", hash = "sha256:d9726d2c9bd6aed8cb994d89b3910ca0079406edce3670886ec828a73e7bdd53" }, + { url = "https://mirrors.aliyun.com/pypi/packages/8b/a3/297207726b292e85b9a8ce24ef6ab16a056c457100e915a67b6928a58fa9/websockets-13.0-cp312-cp312-win_amd64.whl", hash = "sha256:fa0839f35322f7b038d8adcf679e2698c3a483688cc92e3bd15ee4fb06669e9a" }, + { url = "https://mirrors.aliyun.com/pypi/packages/03/b6/778678e1ff104df3a869dacb0bc845df34d74f2ff7451f99babccd212203/websockets-13.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:da7e501e59857e8e3e9d10586139dc196b80445a591451ca9998aafba1af5278" }, + { url = "https://mirrors.aliyun.com/pypi/packages/fa/25/28609b2555f11e4913a4021147b7a7c5117b5c41da5d26a604a91bae85b9/websockets-13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a00e1e587c655749afb5b135d8d3edcfe84ec6db864201e40a882e64168610b3" }, + { url = "https://mirrors.aliyun.com/pypi/packages/cb/1f/e06fb15fde90683fd98e6ca44fb54fe579161ce553d54fdbb578014ae1a7/websockets-13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a7fbf2a8fe7556a8f4e68cb3e736884af7bf93653e79f6219f17ebb75e97d8f0" }, + { url = "https://mirrors.aliyun.com/pypi/packages/22/00/9892eee346f44cd814c18888bc1a05880e3f8091e4eb999e6b34634cd278/websockets-13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ea9c9c7443a97ea4d84d3e4d42d0e8c4235834edae652993abcd2aff94affd7" }, + { url = "https://mirrors.aliyun.com/pypi/packages/dc/ad/2bdc3a5dd60b639e0f8e76ee4a57fda27abaf05f604708c61c6fd7f8ad88/websockets-13.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35c2221b539b360203f3f9ad168e527bf16d903e385068ae842c186efb13d0ea" }, + { url = "https://mirrors.aliyun.com/pypi/packages/0c/14/5585de16939608b77a37f8b88e1bd1d430d95ec19d3a8c26ec42a91f2815/websockets-13.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:358d37c5c431dd050ffb06b4b075505aae3f4f795d7fff9794e5ed96ce99b998" }, + { url = "https://mirrors.aliyun.com/pypi/packages/7b/1e/6cd9063fd34fe7f649ed9a56d3c91e80dea95cf3ab3344203ee774d51a56/websockets-13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:038e7a0f1bfafc7bf52915ab3506b7a03d1e06381e9f60440c856e8918138151" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d9/4d/c3282f8e54103f3d38b5e56851d00911dafd0c37c8d03a9ecc7a25f2a9da/websockets-13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fd038bc9e2c134847f1e0ce3191797fad110756e690c2fdd9702ed34e7a43abb" }, + { url = "https://mirrors.aliyun.com/pypi/packages/a1/08/af4f67b74cc6891ee1c34a77b47a3cb77081b824c3df92c1196980df9a4f/websockets-13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93b8c2008f372379fb6e5d2b3f7c9ec32f7b80316543fd3a5ace6610c5cde1b0" }, + { url = "https://mirrors.aliyun.com/pypi/packages/b4/b7/2c991e51d48b1b98847d0a0b608508a3b687f215a2390f99cf0ee7dd2777/websockets-13.0-cp313-cp313-win32.whl", hash = "sha256:851fd0afb3bc0b73f7c5b5858975d42769a5fdde5314f4ef2c106aec63100687" }, + { url = "https://mirrors.aliyun.com/pypi/packages/bc/0f/f06ed6485cf9cdea7d89c2f6e9d19f1be963ba5d26fb79760bfd17dd4aa5/websockets-13.0-cp313-cp313-win_amd64.whl", hash = "sha256:7d14901fdcf212804970c30ab9ee8f3f0212e620c7ea93079d6534863444fb4e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/b2/89/c0be9f09eea478659e9d936210ff03e6a2a3a8d4b8dfac6b1143ff646ded/websockets-13.0-py3-none-any.whl", hash = "sha256:dbbac01e80aee253d44c4f098ab3cc17c822518519e869b284cfbb8cd16cc9de" }, +]